diff --git a/.gitignore b/.gitignore index fcf5e6a1..528fa6d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ ### CUSTOM ### *.exe +*.msi *.tar.xz *.zip *.spec @@ -9,15 +10,33 @@ tmp +*.tar *.tar.* +*.tgz *.zip +*.html +*.pdf +*.odt + +doc/dev/images/auto_* +doc/dev/documentation.* +!doc/dev/*.org + +doc/dev/images/auto_* +doc/users/documentation.* +!doc/users/*.org + +TAGS + pamhyrBuild wine-pamhyr-build* _PAMHYR_ __old__ +*-venv + ### END CUSTOM ### # Created by https://www.toptal.com/developers/gitignore/api/python @@ -309,3 +328,318 @@ compile_commands.json *_qmlcache.qrc # End of https://www.toptal.com/developers/gitignore/api/qt + +# Created by https://www.toptal.com/developers/gitignore/api/latex +# Edit at https://www.toptal.com/developers/gitignore?templates=latex + +### LaTeX ### +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 +.*.lb + +## Intermediate documents: +*.dvi +*.xdv +*-converted-to.* +# these rules might exclude image files for figures etc. +# *.ps +# *.eps +# *.pdf + +## Generated if empty string is given at "Please type another file name for output:" +.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex(busy) +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Build tool directories for auxiliary files +# latexrun +latex.out/ + +## Auxiliary and intermediate files from other packages: +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.pre +*.snm +*.vrb + +# changes +*.soc + +# comment +*.cut + +# cprotect +*.cpt + +# elsarticle (documentclass of Elsevier journals) +*.spl + +# endnotes +*.ent + +# fixme +*.lox + +# feynmf/feynmp +*.mf +*.mp +*.t[1-9] +*.t[1-9][0-9] +*.tfm + +#(r)(e)ledmac/(r)(e)ledpar +*.end +*.?end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls +*.glsdefs +*.lzo +*.lzs +*.slg +*.slo +*.sls + +# uncomment this for glossaries-extra (will ignore makeindex's style files!) +# *.ist + +# gnuplot +*.gnuplot +*.table + +# gnuplottex +*-gnuplottex-* + +# gregoriotex +*.gaux +*.glog +*.gtex + +# htlatex +*.4ct +*.4tc +*.idv +*.lg +*.trc +*.xref + +# hyperref +*.brf + +# knitr +*-concordance.tex +# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files +# *.tikz +*-tikzDictionary + +# listings +*.lol + +# luatexja-ruby +*.ltjruby + +# makeidx +*.idx +*.ilg +*.ind + +# minitoc +*.maf +*.mlf +*.mlt +*.mtc[0-9]* +*.slf[0-9]* +*.slt[0-9]* +*.stc[0-9]* + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# newpax +*.newpax + +# nomencl +*.nlg +*.nlo +*.nls + +# pax +*.pax + +# pdfpcnotes +*.pdfpc + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# scrwfile +*.wrt + +# svg +svg-inkscape/ + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# pdfcomment +*.upa +*.upb + +# pythontex +*.pytxcode +pythontex-files-*/ + +# tcolorbox +*.listing + +# thmtools +*.loe + +# TikZ & PGF +*.dpth +*.md5 +*.auxlock + +# titletoc +*.ptc + +# todonotes +*.tdo + +# vhistory +*.hst +*.ver + +# easy-todo +*.lod + +# xcolor +*.xcp + +# xmpincl +*.xmpi + +# xindy +*.xdy + +# xypic precompiled matrices and outlines +*.xyc +*.xyd + +# endfloat +*.ttt +*.fff + +# Latexian +TSWLatexianTemp* + +## Editors: +# WinEdt +*.bak +*.sav + +# Texpad +.texpadtmp + +# LyX +*.lyx~ + +# Kile +*.backup + +# gummi +.*.swp + +# KBibTeX +*~[0-9]* + +# TeXnicCenter +*.tps + +# auto folder when using emacs and auctex +./auto/* +*.el + +# expex forward references with \gathertags +*-tags.tex + +# standalone packages +*.sta + +# Makeindex log files +*.lpz + +# xwatermark package +*.xwm + +# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib +# option is specified. Footnotes are the stored in a file with suffix Notes.bib. +# Uncomment the next line to have this generated file ignored. +#*Notes.bib + +### LaTeX Patch ### +# LIPIcs / OASIcs +*.vtc + +# glossaries +*.glstex + +# End of https://www.toptal.com/developers/gitignore/api/latex diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5f12c3e7..31672cf2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,8 +16,9 @@ stages: - downloads - - build + - configure - test + - build - package - release @@ -32,7 +33,7 @@ dl-mage-linux: rules: - if: $CI_COMMIT_BRANCH == 'ci-test' || $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG script: - - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test6/downloads/packages/mage_linux.tgz + - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/v8.3.0/downloads/packages/mage_linux.tgz - mkdir -p mage-linux - cd mage-linux - tar xvf ../mage.tgz @@ -49,7 +50,7 @@ dl-mage-windows: rules: - if: $CI_COMMIT_BRANCH == 'ci-test' || $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG script: - - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test6/downloads/packages/mage_windows.tgz + - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/v8.3.0/downloads/packages/mage_windows.tgz - mkdir -p mage-windows - cd mage-windows - tar xvf ../mage.tgz @@ -59,12 +60,12 @@ dl-mage-windows: - mage-windows/mage_extraire.exe - mage-windows/mailleurPF.exe -######### -# BUILD # -######### +############# +# CONFIGURE # +############# set-version: - stage: build + stage: configure tags: - linux script: @@ -75,7 +76,7 @@ set-version: - VERSION build-lang: - stage: build + stage: configure tags: - linux script: @@ -85,6 +86,49 @@ build-lang: paths: - src/lang/*.qm +######### +# TESTS # +######### + +unittest: + stage: test + tags: + - linux + needs: + - job: set-version + artifacts: true + script: + - python3 -m venv venv + - . venv/bin/activate + - pip3 install -U pip + - pip3 install -r ./full-requirements.txt + - pip3 install -U -r ./full-requirements.txt + - cd src + - python3 -m unittest discover -t . + +test-pep8: + stage: test + tags: + - linux + needs: + - job: set-version + artifacts: true + - job: unittest + script: + # Setup virtual env + - python3 -m venv venv + - . venv/bin/activate + - pip3 install -U pip + - pip3 install -r ./requirements.txt + - pip3 install -U -r ./requirements.txt + - pip3 install pycodestyle + - pycodestyle --exclude="*_to_*.py" ./src + allow_failure: true + +######### +# BUILD # +######### + build-users-doc: stage: build tags: @@ -98,6 +142,9 @@ build-users-doc: artifacts: paths: - doc/users/documentation.pdf + - doc/users/documentation.html + - doc/users/images/ + allow_failure: true build-developers-doc: stage: build @@ -109,9 +156,26 @@ build-developers-doc: script: - cd ./doc/dev/ - ./build.sh + # Create html documentation + - cd ../../src + - python3 -m venv doc-venv + - . doc-venv/bin/activate + - pip3 install -U pip + - pip3 install -r ../full-requirements.txt + - pip3 install -U -r ../full-requirements.txt + - pip3 install pdoc3 + - pdoc --html . + # Create archive + - tar cf html.tar ./html + - cd ../doc/dev + - mv ../../src/html.tar ./ artifacts: paths: - doc/dev/documentation.pdf + - doc/dev/documentation.html + - doc/dev/html.tar + - doc/dev/images/ + allow_failure: true build-linux: stage: build @@ -136,39 +200,50 @@ build-linux: # Setup virtual env - python3 -m venv linux-venv - . linux-venv/bin/activate + - pip3 install -U pip - pip3 install -r ../requirements.txt + - pip3 install -U -r ../requirements.txt # Run Pyinstaller - pyinstaller -y --paths linux-venv/lib/python3.8/site-packages ../src/pamhyr.py # Create directory - mkdir -p pamhyr - mkdir -p pamhyr/pamhyr + - mkdir -p pamhyr/pamhyr/_internal # Copy Pamhyr info - - cp -v ../VERSION pamhyr/pamhyr/ - - cp -v ../AUTHORS pamhyr/pamhyr/ - - cp -v ../LICENSE pamhyr/pamhyr/ + - cp -v ../VERSION pamhyr/pamhyr/_internal/ + - cp -v ../AUTHORS pamhyr/pamhyr/_internal/ + - cp -v ../LICENSE pamhyr/pamhyr/_internal/ + - cp -v ../src/motd.txt pamhyr/pamhyr/_internal/ # Copy mage - mkdir -p pamhyr/mage - cp -v ../mage-linux/* pamhyr/mage/ # Copy Pamhyr - cp -r dist/pamhyr pamhyr/ # Copy Pamhyr resources - - mkdir -p pamhyr/pamhyr/View - - mkdir -p pamhyr/pamhyr/View/ui - - mkdir -p pamhyr/pamhyr/View/ui/Widgets - - cp -r ../src/View/ui/*.ui pamhyr/pamhyr/View/ui/ - - cp -r ../src/View/ui/ressources/ pamhyr/pamhyr/View/ui - - cp -r ../src/View/ui/Widgets/*.ui pamhyr/pamhyr/View/ui/Widgets + - mkdir -p pamhyr/pamhyr/_internal/View + - mkdir -p pamhyr/pamhyr/_internal/View/ui + - mkdir -p pamhyr/pamhyr/_internal/View/ui/Widgets + - cp -r ../src/View/ui/*.ui pamhyr/pamhyr/_internal/View/ui/ + - cp -r ../src/View/ui/ressources/ pamhyr/pamhyr/_internal/View/ui + - cp -r ../src/View/ui/Widgets/*.ui pamhyr/pamhyr/_internal/View/ui/Widgets # Copy lang - - mkdir -p pamhyr/pamhyr/lang - - cp -r ../src/lang/*.qm pamhyr/pamhyr/lang + - mkdir -p pamhyr/pamhyr/_internal/lang + - cp -r ../src/lang/*.qm pamhyr/pamhyr/_internal/lang # Copy tests cases - mkdir -p pamhyr/tests_cases/ - mkdir -p pamhyr/tests_cases/Saar - cp ../tests_cases/Saar/Saar.pamhyr pamhyr/tests_cases/Saar/ # Copy documentations - mkdir -p pamhyr/doc/ + - mkdir -p pamhyr/doc/images/ + - mkdir -p pamhyr/doc/html/ - cp ../doc/users/documentation.pdf pamhyr/doc/Pamhyr2-users.pdf + - cp ../doc/users/documentation.html pamhyr/doc/Pamhyr2-users.html - cp ../doc/dev/documentation.pdf pamhyr/doc/Pamhyr2-dev.pdf + - cp ../doc/dev/documentation.html pamhyr/doc/Pamhyr2-dev.html + #- cp ../doc/users/images/* pamhyr/doc/images + - cp ../doc/dev/images/* pamhyr/doc/images + - cp ../doc/images/* pamhyr/doc/images # Create running script - echo "#!/bin/sh" > pamhyr/Pamhyr2 - echo "./pamhyr/pamhyr $@" >> pamhyr/Pamhyr2 @@ -199,6 +274,7 @@ build-windows: - cd windows # Setup env - python -m pip install -r ..\requirements.txt + - python -m pip install -U -r ..\requirements.txt # Run Pyinstaller - pyinstaller -y ..\src\pamhyr.py # Create directory @@ -210,36 +286,6 @@ build-windows: paths: - windows/pamhyr -######### -# TESTS # -######### - -# test-windows: -# stage: test -# tags: -# - wine -# needs: -# - job: set-version -# artifacts: true -# - job: build-windows -# artifacts: true -# script: -# - cd windows\pamhyr -# - pamhyr\pamhyr.exe hello - -# test-linux: -# stage: test -# tags: -# - linux -# needs: -# - job: set-version -# artifacts: true -# - job: build-linux -# artifacts: true -# script: -# - cd linux/pamhyr -# - ./Pamhyr2 hello - ############ # PACKAGES # ############ diff --git a/doc/dev/build.sh b/doc/dev/build.sh index 476dd5ad..2e8deb56 100755 --- a/doc/dev/build.sh +++ b/doc/dev/build.sh @@ -1,10 +1,18 @@ #! /bin/sh -# Build the PDF - -emacs -Q -q -nw --batch \ +echo " PDF documentation.org" +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-latex-export-to-pdf + +echo " HTML documentation.org" +emacs -nw --batch \ + --eval "(require 'org)" \ + --eval "(require 'ox-latex)" \ + --eval "(require 'ox-html)" \ + --eval "(load-file \"../tools/setup.el\")" \ + --file ./documentation.org \ + -f org-html-export-to-html diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index e245b959..bbef8dc2 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -73,7 +73,7 @@ https://doc.qt.io/qt-5/model-view-programming.html (last access [fn:org] The org-mode website: https://orgmode.org/ (last access 2023-09-15) -* TODO Architecture +* Architecture Pamhyr2's architecture is based on Qt Model/View, see Figure [[graph-architecture]]. It is made up of several different components: the @@ -97,35 +97,68 @@ simulation results to model data. #+name: graph-architecture #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="9cm", data=*this*, name="graph-architecture", caption="Pamhyr2 Model/View architecture scheme (inspired by Qt Model/View architecture [[https://doc.qt.io/qt-5/model-view-programming.html]])", float="t") -#+begin_src dot :file "images/graph-architecture.png" :cache no +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-architecture", caption="Pamhyr2 Model/View architecture scheme (inspired by Qt Model/View architecture [[https://doc.qt.io/qt-5/model-view-programming.html]])", float="t") +#+begin_src dot :file "images/auto_graph-architecture.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=white]; + edge[colorscheme=set19,color=0]; + + root[style=invis]; + + subgraph cluster0 { + label="File System" + style=dashed; + + save[label="Pamhyr save",fillcolor="9",shape=note]; + sbin[label="Solver binary",fillcolor="9",shape=note]; + configfile[label="Pamhyr configuration file",fillcolor="9",shape=note]; + } - save[label="Pamhyr save",fillcolor="9",shape=note]; - model[label="Model",fillcolor="2"]; - view[label="View",fillcolor="1"]; - delegate[label="Delegate",fillcolor="3"]; - action[label="Action",fillcolor="3"]; - solver[label="Solver",fillcolor="6"]; - undocommand[label="Command",fillcolor="4"]; user[label="User",shape=ellipse]; - model -> save[label="Save"]; - save -> model[label="Load"]; + subgraph cluster1 { + label="Pamhyr2"; + + config[label="Configuration",fillcolor="5"]; + model[label="Model",fillcolor="2"]; + view[label="View",fillcolor="1"]; + delegate[label="Delegate",fillcolor="3"]; + action[label="Action",fillcolor="3"]; + solver[label="Solver",fillcolor="6"]; + undocommand[label="Command",fillcolor="4"]; + } + + root -> model[style=invis]; + root -> config[style=invis]; + + model -> save[dir=both,label="Save/Load"]; + config -> configfile[dir=both,label="Save/Load"]; + + undocommand -> solver[style=invis]; + action -> solver[style=invis]; + delegate -> solver[style=invis]; model -> view[label="Rendering"]; view -> delegate[label="Rendering"]; delegate -> undocommand[label="Create"]; - action -> undocommand[label="Create"]; + action -> undocommand[label="Create/use"]; action -> solver[label="Run"]; - solver -> model[label="Add results"]; + solver -> model[dir=both,label="Export/Results",labelangle=0,labelfloat=true,constraint=true]; + solver -> sbin[dir=both,label="Execute/Results"]; undocommand -> model[label="Modify"]; view -> user[label="Vizualize"]; user -> delegate[label="Modify"]; - user -> action[label="Activate"]; + user -> action[label="Triggere"]; + + config -> solver[label="Create",style=dashed,labelfloat=true,constraint=true]; + //model -> config[style=invis]; + config -> model[style=invis]; + delegate -> action[style=invis]; + save -> configfile[style=invis]; + root -> solver[style=invis]; + user -> solver[style=invis]; } #+end_src @@ -134,130 +167,684 @@ All the model source code are in the directory {{{file(src/Model)}}} delegate and command are in {{{file(src/View)}}} (see section [[View]]). Solvers classes are in {{{file(src/Solver)}}} (see section [[Solver]]). +The following sub section show examples of main {{{pamhyr}}} internal +class for view componants, but this documentation is not exhaustive, +be free to watch existing code for more details and examples. In, +addition some features are not factorise and must be implemented from +scratch (directly with Qt for example). + [fn:qt-mv] The Qt Model/View documentation web page: https://doc.qt.io/qt-5/model-view-programming.html -** TODO Model +** Model -- Model de donnée Python -- Correspond à une sauvegarde SQL +The model is a set of Python classes. In Pamhyr2, this classes must +respect some constraint. Each model class must inherits +=Model.Tools.SQLSubModel= abstract class, except the =Model.Study= +class who inherits =Model.Tools.SQLModel= (see [[SQL]]). + +The model entry point is the Study class. It contains infomation about +the study: their name, description, time system, and so on. Their +contains a River object too. This river object inherits the network +graph and contains a list of =RiverNode= and a list of =RiverReach= +(an edge who contains a source node, and destination node). +=RiverReach= contrains geometry, so, the river network (node and edge) +associated with the geometry forms the basis of the model, and the +other components are linked to one of these basic components. #+name: graph-model #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies", float="t") -#+begin_src dot :file "images/graph-model.png" :cache no +#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies (A -> B means A can contain references to B)", float="t") +#+begin_src dot :file "images/auto_graph-model.png" :cache no digraph { - bgcolor="transparent"; - node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; - study[label="Study"]; - river[label="River"]; + //subgraph cluster0 { + // style=dashed; + study[label="Study"]; + river[label="River"]; - subgraph cluster00 { - label="Network" - rnode[label="RiverNode"]; - redge[label="RiverReach"]; - } + subgraph cluster00 { + style=solid; + label="Network" + rnode[label="RiverNode"]; + redge[label="RiverReach"]; + } - frictionlist[label="FrictionList"]; + subgraph cluster06 { + style=solid; + label="Greometry" + georeach[label="Reach"]; + geocrosssection[label="Cross-section"]; + geopoint[label="Point"]; + } + //} - subgraph cluster01 { - label="Stricklers"; - stricklers[label="Stricklers"]; - stricklerslist[label="StricklersList"]; - } - subgraph cluster02 { - label="BoundaryCondition"; - boundaryconditionlist[label="BoundaryConditionList"]; - boundarycondition[label="BoundaryCondition"]; - } - subgraph cluster03 { - label="LateralContribution"; - lateralcontributionlist[label="LateralContributionList"]; - lateralcontribution[label="LateralContribution"]; - } - subgraph cluster04 { - label="InitialConditions"; - initialconditionsdict[label="InitialConditionsDict"]; - initialconditions[label="InitialConditions"]; - } + //subgraph cluster1 { + // style=dashed; + frictionlist[label="FrictionList"]; - solverparameterslist[label="SolverParametersList"]; + subgraph cluster01 { + style=solid; + label="Stricklers"; + stricklers[label="Stricklers"]; + stricklerslist[label="StricklersList"]; + } + subgraph cluster02 { + style=solid; + label="BoundaryCondition"; + boundaryconditionlist[label="BoundaryConditionList"]; + boundarycondition[label="BoundaryCondition"]; + } + subgraph cluster03 { + style=solid; + label="LateralContribution"; + lateralcontributionlist[label="LateralContributionList"]; + lateralcontribution[label="LateralContribution"]; + } + subgraph cluster04 { + style=solid; + label="InitialConditions"; + initialconditionsdict[label="InitialConditionsDict"]; + initialconditions[label="InitialConditions"]; + } - subgraph cluster05 { - label="Sediment"; - sedimentlayerlist[label="SedimentLayerList"]; - sedimentlayer[label="SedimentLayer"]; - layer[label="Layer"]; - } + solverparameterslist[label="SolverParametersList"]; - subgraph cluster06 { - label="Greometry" - georeach[label="Reach"]; - geocrosssection[label="Cross-section"]; - geopoint[label="Point"]; - } + subgraph cluster05 { + style=solid; + label="Sediment"; + sedimentlayerlist[label="SedimentLayerList"]; + sedimentlayer[label="SedimentLayer"]; + layer[label="Layer"]; + } + //} - subgraph cluster07 { - label="Results" - results[label="Results"] - rriver[label="River"]; - rreach[label="Reach"]; - rcrosssection[label="Cross-section"]; - } + subgraph cluster2 { + style=dashed; + label="Results" + results[label="Results"] + rriver[label="River"]; + rreach[label="Reach"]; + rcrosssection[label="Cross-section"]; + } - study -> river; - river -> rnode; - river -> redge; - redge -> rnode; - river -> boundaryconditionlist -> boundarycondition -> rnode; - river -> lateralcontributionlist -> lateralcontribution -> redge; - river -> initialconditionsdict -> initialconditions; - initialconditions -> redge; - river -> stricklerslist -> stricklers; - river -> solverparameterslist; - river -> sedimentlayerlist -> sedimentlayer -> layer; - redge -> frictionlist -> stricklers; - redge -> georeach -> geocrosssection -> geopoint; - geocrosssection -> sedimentlayer; - geopoint -> sedimentlayer; + study -> river; + river -> rnode; + river -> redge; + redge -> rnode; + river -> boundaryconditionlist -> boundarycondition -> rnode; + river -> lateralcontributionlist -> lateralcontribution -> redge; + river -> initialconditionsdict -> initialconditions; + initialconditions -> redge; + river -> stricklerslist -> stricklers; + river -> solverparameterslist; + river -> sedimentlayerlist -> sedimentlayer -> layer; + redge -> frictionlist -> stricklers; + redge -> georeach -> geocrosssection -> geopoint; + geocrosssection -> sedimentlayer; + geopoint -> sedimentlayer; - results -> study[style="dashed"]; - results -> rriver; - rriver -> river[style="dashed"]; - rriver -> rreach; - rreach -> georeach[style="dashed"]; - rreach -> rcrosssection; - rcrosssection -> geocrosssection[style="dashed"]; + results -> study; + results -> rriver; + rriver -> river; + rriver -> rreach; + rreach -> georeach; + rreach -> rcrosssection; + rcrosssection -> geocrosssection; + + // river -> boundaryconditionlist -> boundarycondition -> results[style=invis]; + // river -> lateralcontributionlist -> lateralcontribution -> results[style=invis]; + // river -> initialconditionsdict -> initialconditions -> results[style=invis]; + // initialconditions -> results[style=invis]; + // river -> stricklerslist -> stricklers -> results[style=invis]; + // river -> solverparameterslist -> results[style=invis]; + // river -> sedimentlayerlist -> sedimentlayer -> layer -> results[style=invis]; + + geopoint -> boundaryconditionlist[style=invis]; + geopoint -> lateralcontributionlist[style=invis]; + geopoint -> initialconditionsdict[style=invis]; + geopoint -> initialconditions[style=invis]; + geopoint -> stricklerslist[style=invis]; + geopoint -> solverparameterslist[style=invis]; + geopoint -> sedimentlayerlist[style=invis]; } #+end_src -*** TODO SQL study file -*** TODO List class -*** TODO Dict class -** TODO View -*** TODO UI file -*** TODO Translate -*** TODO Window -*** TODO UndoCommand -*** TODO Table -*** TODO Plot -*** TODO Mainwindow -** TODO Solver +*** SQL + +The model must be export to a database file to create a study save +file. This file use SQLite3[fn:sqlite] format and the extention +=.pamhyr=. So, each model componante must be register into this study +file. To create, update, set and get information into SQLite database +we use SQL command. The database use version number and some +modification could be perform to update database. For each model +componante, correspond one or more SQL table to store information. To +normalize the interaction with database we made two classes, SQLModel +and SQLSubModel. The Study class use SQLModel because is the top of +the model hierachy. The rest of model class inherits to SQLSubModel. + +A class who inherits SQLSubModel, must implement some methods: +- =_sql_create=: Class method to create the database scheme +- =_sql_update=: Class method to update the database scheme if necessary +- =_sql_load=: Class method to load data from DB +- =_sql_save=: Method to save current object into DB + +Class method take in arguments: The class (=cls=), a function to +execute SQL command into the database (=execute=). In addition, the +update method take the previous version of database, load method take +an optional arguments =data= if additional infomation ar needed, and +who can contains whatever you want. The method save take in arguments +the current object (=self=), a function to execute SQL command into +the database (=execute=), and optional data (=data=). + +The class who inherits SQLSubModel can also define an class attribute +=_sub_classes= to set a formal class dependencies into database. This +attribute is use at database creation to create all table, and at +update to update all the database table. Let see examples of +SQLSubModel usage for two classes Foo and Bar with Foo contains list +of Bar (Listing [[sql-bar]] and [[sql-foo]]). + +#+NAME: sql-bar +#+CAPTION: Exemple of class Bar inherits SQLSubModel. +#+begin_src python :python python3 :results output :noweb yes + from Model.Tools.PamhyrDB import SQLSubModel + + class Bar(SQLSubModel): + _id_cnt = 0 + def __init__(self, id = -1, x = 0, y = 0): + self._x = x + self._y = y + if id == -1: + self.id = Bar._id_cnt + 1 + else: + self.id = id + Bar._id_cnt = max(id, Bar._id_cnt+1) + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE bar ( + id INTEGER NOT NULL PRIMARY KEY, + x INTEGER NOT NULL, + foo_id INTEGER NOT NULL, + FOREIGN KEY(foo_id) REFERENCES foo(id), + )""") + return True + + @classmethod + def _sql_update(cls, execute, version): + # If version is lesser than 0.0.2, add column to bar table + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + execute("ALTER TABLE bar ADD COLUMN y INTEGER") + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + table = execute( + f"SELECT id, x, y FROM bar WHERE foo_id = {data['id']}" + ) + for row in table: + bar = cls( + id = row[0], x = row[1], y = row[2], + ) + new.append(bar) + return new + + def _sql_save(self, execute, data = None): + execute("INSERT INTO bar (id,x,y,foo_id) VALUES " + + f"({self.id}, {self._x}, {self._y}, {data['id']})") +#+end_src + +#+NAME: sql-foo +#+CAPTION: Exemple of class Foo inherits SQLSubModel and contains a list of Bar object (Listing [[sql-bar]]). +#+begin_src python :python python3 :results output :noweb yes + class Foo(SQLSubModel): + _id_cnt = 0 + _sub_classes = [Bar] + + def __init__(self, id = -1, name = ""): + self._name = name + self._bar = [] + # ... + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE foo ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + ) + """) + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, excute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + table = execute( + "SELECT id, name FROM foo" + ) + for row in table: + foo = cls( + id = row[0], + name = row[1], + ) + data = { + "id": row[0], # Current Foo ID + } + foo._bar = Bar._sql_load(execute, data=data) + new.append(foo) + return new + + def _sql_save(self, execute, data = None): + execute(f"DELETE FROM foo WHERE id = {self.id}") + execute(f"DELETE FROM bar WHERE foo_id = {self.id}") + # Save new data + execute(f"INSERT INTO bar (id,name) VALUES ({self.id}, {self._name})") + data = {"id": self.id} + for bar in self._bar: + bar._sql_save(execute, data=data) +#+end_src + +[fn:sqlite] The SQLite web site: https://www.sqlite.org/index.html +(last access 2023-09-20) + +*** List class + +A abstract class PamhyrModelList is available and provide some of +basic methods for object list in Model. This abstract class implement +some classic action in View like: insert new object, delete object, +sort, move object up, move object down, and so on. An variant exists +for multiple list with same type of object, each sublist is called +tab, because in View, this kind of list si prensented in different +table PamhyrModelListWithTab. + +*** Dict class + +A abstract class PamhyrModelDict is available and provide some of +basic methods for object dictionary in Model. This class is like +PamhyrModelList but use a dictionary instead of list. + +** View + +Pamhyr2 use Qt as graphical user interface library with the +application "Qt designer" for windows or widget creation (see [[UI file]]) +and "Qt linguist" for interface translate (see [[Translate]]). In +addition, we use matplotlib as ploting library (see [[Plot]]). + +Typically, each model componant have an associated window in +application to add, delete or edit this componant. At top level of +View directory we found the =MainWindow.py= file and some +sub-directories. A view sub-directory contains: A =Window.py= file, a +=Table.py= file with table model definition if nessessary, one or more +=Plot*.py= file with plot class definition, a =translate.py= file with +componant translate, and possible other files or sub-directories. + +*** UI file + +We define as possible all Pamhyr2 windows and custom widgets with "Qt +designer". This application generate UI file who describes interface +organisation with table, layout, button, etc. This method is faster +than hand made windows and widget creation, and saves us some purely +descriptive code. The UI files are saved into =src/View/ui= for +window, and =/src/View/ui/Widgets= for custom widget. + +*** Translate + +#+NAME: pamhyr-trad +#+CAPTION: Example of =PamhyrTranslate= class implementation with a global traduction for /FooBar/ and a additional dictionary =table_headers= +#+begin_src python :python python3 :results output :noweb yes + from PyQt5.QtCore import QCoreApplication + from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate + + class MyTranslate(PamhyrTranslate): + def __init__(self): + super(MyTranslate, self).__init__() + + # Add traduction to global dictionary + self._dict["My"] = _translate("My", "FooBar") + # Add an additional translate dictionary + self._sub_dict["table_headers"] = { + "foo": _translate("My", "Foo"), + "bar": _translate("My", "Bar"), + "baz": _translate("My", "Baz"), + } +#+end_src + +*** Window + +The abstract class PamhyrWindow and PamhyrDialog are used for most of +Pamhyr2 window. These class allow to create an window for Pamhyr2 GUI +and implemente some useful methods. The super class method difine some +generic value from optional parameters, for examples: +- =self._study=: The study giving in constructor parameters =study= + (typically a =Model.Study= class object) +- =self._config=: The configuration giving in constructor parameters + =config= (typically a =Config= class object) +- =self._trad=: The traductor dictionary giving in constructor + parameters =trad= (typically a =Model.Tools.PamhyrTranslate= class + object) + +#+NAME: window +#+CAPTION: Example of Pamhyr2 window +#+begin_src python :python python3 :results output :noweb yes + from View.Tools.PamhyrWindow import PamhyrWindow + from View.My.Translate import MyTranslate + from View.My.Table import MyTableModel + + class MyWindow(PamhyrWindow): + _pamhyr_ui = "MyUI" + _pamhyr_name = "My window" + + def __init__(self, my_data=None, + study=None, config=None, + parent=None): + self._my_data = my_data + + super(MyWindow, self).__init__( + # Window title + title = self._pamhyr_name + " - " + study.name, + # Window standard data + study = study, config = config, + trad = MyTranslate(), + parent = parent, + # Activate undo/redo and copy/paste shortcut + options = ["undo", "copy"] + ) + + # Add custom data to hash window computation + self._hash_data.append(self._my_data) + + # Setup custom window components + self.setup_table() + self.setup_connections() + + def setup_table(self): + # Init table(s)... + + def setup_connections(self): + # Init action connection(s)... + + # ... +#+end_src + +Typically we called method =setup_*=, the method to initialize some +window componants or connections. + +*** Table + +An abstract class PamhyrTableModel is available to define a simple +QAbstractTableModel shortly. In simple cases, there are only =data= +and =setData= methode to implement, but the constructor needs more +information than a classic QAbstractTableModel class. + +#+NAME: table-src +#+CAPTION: Definition of a table model from =PamhyrTableModel= in a file =View/My/Table.py=. +#+begin_src python :python python3 :results output :noweb yes + from View.Tools.PamhyrTable import PamhyrTableModel + + class MyTableModel(PamhyrTableModel): + def data(self, index, role): + # Retrun data at INDEX... + + @pyqtSlot() + def setData(self, index, value, role=Qt.EditRole): + # Set VALUE at INDEX... +#+end_src + +#+CAPTION: Using the table model defined in Listing [[table-src]] in window funtion =setup_table= defined Listing [[window]]. +#+begin_src python :python python3 :results output :noweb yes + # Table model creation (Window.py: setup_table) + table_headers = self._trad.get_dict("table_headers") + self._model = MyTableModel( + table_view = table, # The table view object + table_headers = table_headers, # The table column headers dict + # (with traduction) + editable_headers = ["foo", "bar"], # List of editable column name + delegates = { + "bar": self.my_delegate, # Custom delegate for column 'bar' + }, + data = self._my_lst, # The data + undo = self._undo_stack, # The window undo command stack + ) +#+end_src + +*** UndoCommand + +All model modification must be done by an QUndoCommand, this command +allow to undo and redo an action. This a Qt class wi can inherit to +define custom undo command (see example Listing [[undo-cmd]]) + +#+NAME: undo-cmd +#+CAPTION: Example of custom UndoCommand, this command allow to add a node to graph in river network window (method redo), and delete it to graph with undo method +#+begin_src python :python python3 :results output :noweb yes +class AddNodeCommand(QUndoCommand): + def __init__(self, graph, node): + QUndoCommand.__init__(self) + + self._graph = graph + self._node = node + + def undo(self): + self._graph.remove_node(self._node.name) + + def redo(self): + self._graph.insert_node(self._node) +#+end_src + +All undo command must be push into a =QUndoStack= (see Listing +[[undo-cmd-push]]) to perform the action and allow user undo and redo this +action. In PamhyrWindow (and PamhyrDialog) the undo stack is +automatically create if the option ="undo"= is activate at window +creation, this stack is accessible at =self._undo_stack=. + +#+NAME: undo-cmd-push +#+CAPTION: Example of UndoCommand push into an undo stack. +#+begin_src python :python python3 :results output :noweb yes + self._undo_stack.push( + AddNodeCommand( + self._graph, + node + ) + ) +#+end_src + +*** Plot + +To define a new plot you can create a class who inherit to APlot. The +creator need threee argument: + - A =canvas= of type =MplCanvas= + - A =data= used in =draw= and =update= to create and update the plot + - A optional =toolbar= of type =PamhyrToolbar= +This class must implement two method =draw= and =update=, the first +method to draw the plot from scratch, the second to update the plot if +data has changed. + +#+begin_src python :python python3 :results output :noweb yes + from View.Tools.PamhyrPlot import PamhyrPlot + + class MyPlot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None): + super(MyPlot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + def draw(self): + # Draw function code... + + def update(self, ind = None): + # Update function code... +#+end_src + +** Solver + +The Pamhyr2 architecture allow to define multiple solver. A solver is +define by a: + - type + - name + - description, + - path + - command line pattern + - (optional) input formater path + - (optional) input formater command line + - (optional) output formater path + - (optional) output formater command line + +Let see Figure [[graph-multi-solver]], the application can implement +different solver type, this solver type implement the code for export +study to solver input format, and read the solver output to study +results. There exists a generic solver with a generic input and output +format, the type could be use to use a solver not implemented in +Pamhyr2, but this solver must can read/write input and output generic +format or use external script. There is possible to define different +solver with the same type, for example two differents version of the +same solver. Finaly, with input and output formater is possible to +execute a code on distant computer, for example, over ssh. + +#+name: graph-multi-solver +#+header: :results drawer +#+header: :exports results +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-multi-solver", caption="Scheme of multiple solver configured, one Rubarbe solver and two Mage solver with one on local machine and one on a distant machine accessed over ssh", float="t") +#+begin_src dot :file "images/auto_graph-multi-solver.png" :cache no + digraph { + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; + + subgraph cluster00 { + label="User personal computer"; + style=solid; + + subgraph cluster0 { + label="Pamhyr2"; + style=solid; + + // subgraph cluster01 { + // label="Core"; + //model[label="Model", fillcolor=2]; + //view[label="View", fillcolor=1]; + config[label="Configuration", fillcolor=5]; + + // view -> model -> view; + // } + + subgraph cluster02 { + label="Solver"; + style=dashed; + subgraph cluster021 { + label="Solver Classes"; + classSolverM7[label="Mage7", fillcolor=6]; + classSolverM8[label="Mage8", fillcolor=6]; + classSolverR[label="RubarBE", fillcolor=6]; + } + //classSolverX[label="Solver X Binding", fillcolor=6]; + + subgraph cluster022 { + label="Solver Object"; + solverM[label="Mage", fillcolor=6]; + solverM2[label="Mage over ssh", fillcolor=6]; + solverR[label="RubarBE", fillcolor=6]; + //solverX[label="Solver X", fillcolor=6]; + } + + classSolverM8 -> solverM [style=dashed]; + classSolverM8 -> solverM2[style=dashed]; + classSolverR -> solverR[style=dashed]; + //classSolverX -> solverX[style=dashed]; + } + + //config -> solverM[style="dotted"]; + //config -> solverR[style="dotted"]; + //config -> solverX[style="dotted"]; + + //model -> solverM; + //model -> solverM2; + //model -> solverR; + //model -> solverX; + } + + subgraph cluster1 { + label="File System"; + style=dashed; + + mage[label="Mage Binary",shape=note]; + //X[label="Solver X Binary"]; + rubarbe[label="RubarBE Binary",shape=note]; + ssh[label="ssh",shape=note]; + } + } + + //config -> X[style=invis]; + //model -> config[style=invis]; + config -> solverM[label="",constraint=true]; + //config -> solverX[label="",constraint=true]; + config -> solverR[label="",constraint=true]; + config -> solverM2[label="",constraint=true]; + + subgraph cluster2 { + label="Distant server"; + style=solid; + + sshd[label="sshd"]; + + subgraph cluster21 { + label="File System"; + style=dashed; + + mage2[label="Mage Binary",shape=note]; + } + } + + solverM -> mage[label="", color=1]; + mage -> solverM[label="", color=2]; + + //solverX -> X[label="", color=1]; + //X -> solverX[label="", color=2]; + + solverR -> rubarbe[label="", color=1]; + rubarbe -> solverR[label="", color=2]; + + solverM2 -> ssh -> sshd -> mage2[label="", color=1]; + mage2 -> sshd -> ssh -> solverM2[label="", color=2]; + } +#+end_src + +Let see Figure [[graph-pipeline]] the temporal order of action to run a +solver and get results: + - (1) Write solver input file(s) using the study data + - (2) Run the solver + - (2.1) The solver read the input file(s) + - (2.2) The solver compute results and write it to solver output + file(s) + - (3) Pamhyr2 create a Reuslts + - (3.1) The Pamhyr2 solver class read solver output file(s) and + complete Results with readed data #+name: graph-pipeline #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-pipeline", caption="Pamhyr2 solver execution pipeline architecture scheme", float="t") -#+begin_src dot :file "images/graph-pipeline.png" :cache no +#+header: :post attr_wrap(width="10cm", data=*this*, name="graph-pipeline", caption="Pamhyr2 solver execution pipeline architecture scheme", float="t") +#+begin_src dot :file "images/auto_graph-pipeline.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; subgraph cluster0 { label="Pamhyr2" - config[label="Config",fillcolor=5]; + config[label="Configuration",fillcolor=5]; model[label="Model",fillcolor=2]; obj[label="Solver",fillcolor=6]; results[label="Results",fillcolor=2]; @@ -271,26 +858,130 @@ https://doc.qt.io/qt-5/model-view-programming.html obj -> model[style="dashed"]; subgraph cluster1{ - label="System"; - in[label="Solver input files",shape=note]; - out[label="Solver output files",shape=note]; + label="File System"; + style=dashed; + in[label="Solver input file(s)",shape=note,fillcolor=white]; + out[label="Solver output file(s)",shape=note,fillcolor=white]; bin[label="Solver binary",shape=note]; } - obj -> in[label="Write (1)"]; - obj -> bin[label="Execute (2)"]; - bin -> in[label="Read (2.1)"]; - bin -> out[label="Write (2.2)"]; - obj -> results[label="Create (3)"]; - obj -> out[label="Read (3.1)"]; + obj -> in[label="Write (1)",color=1]; + obj -> bin[label="Execute (2)",color=1]; + bin -> in[label="Read (2.1)",color=1]; + bin -> out[label="Write (2.2)",color=2]; + obj -> results[label="Create (3)",color=2]; + obj -> out[label="Read (3.1)", color=2]; view -> model[style="dashed"]; view -> results[style="dashed"]; } #+end_src -** TODO Unit tests -** TODO The debug mode -* TODO Build the project +In case of generic solver (or a solver with input and output formater) +the temporal order of action is prensented in Figure +[[graph-pipeline-generic]]. + +#+name: graph-pipeline-generic +#+header: :results drawer +#+header: :exports results +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-pipeline-generic", caption="Pamhyr2 generic solver execution pipeline architecture scheme", float="t") +#+begin_src dot :file "images/auto_graph-pipeline-generic.png" :cache no + digraph { + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; + + subgraph cluster0 { + label="Pamhyr2" + config[label="Configuration",fillcolor=5]; + model[label="Model",fillcolor=2]; + obj[label="Generic solver",fillcolor=6]; + results[label="Results",fillcolor=2]; + view[label="View",fillcolor=1]; + + results -> model[style="dashed"]; + results -> obj[style="dashed"]; + } + + config -> obj[label=""]; + obj -> model[style="dashed"]; + + subgraph cluster1{ + label="File System"; + style=dashed; + + gin[label="Generic input file", shape=note,fillcolor=white]; + ibin[label="Input formater", shape=note]; + in[label="Solver input file(s)",shape=note,fillcolor=white]; + out[label="Solver output file(s)",shape=note,fillcolor=white]; + gout[label="Generic results file",shape=note,fillcolor=white]; + obin[label="Output formater", shape=note]; + bin[label="Solver binary",shape=note]; + } + + gin -> ibin[style=invis]; + ibin -> bin -> obin[style=invis]; + in -> bin[style=invis]; + obin -> gout[style=invis]; + + // Input format + obj -> gin[label="Write (1)",color=1]; + obj -> ibin[label="Execute (2)",color=1,style=dashed]; + ibin -> gin[label="Read (2.1)",color=1]; + ibin -> in[label="Write (2.2)",color=1]; + + // Solve + obj -> bin[label="Execute (3)",color=1,style=dashed]; + bin -> in[label="Read (3.1)",color=1]; + bin -> out[label="Write (3.2)",color=2]; + + // Output format + obj -> obin[label="Execute (4)",color=2,style=dashed]; + obin -> out[label="Read (4.1)",color=2]; + obin -> gout[label="Write (4.2)",color=2]; + + // Read results + obj -> results[label="Create (5)",color=2]; + obj -> gout[label="Read (5.1)", color=2]; + view -> model[style="dashed"]; + view -> results[style="dashed"]; + } +#+end_src + +To implement a Solver in Pamhyr2, there exists a abstract class +=Solver.AbstractSolver=. A class who herits this class, must implement +different methods: + - =export=: Export the study to solver input file(s) + - =input_param=: Return the solver input parameter(s) as string + - =log_file=: Return the solver log file name as string + - =results=: Read the solver output file(s) and return a + =Model.Results= object. + +** Unit tests + +The unit tests is actually not implemented in Pamhyr2, it is a *work +in progress*. + +** The debug mode + +To activate an deactivate the Pamhyr2 debug mode you can open the +configuration window and type "Ctrl+G" or run Pamhyr2 with command +line: +#+begin_src shell +./Pamhyr2 debug +#+end_src + +This mode add some log and add two action in main window menu: +"About/Debug" open a window with Python Repl in current Python +environement, and "About/Debug SQLite" who open the application +SQLiteBrowser (if installed) on current Study to explore the study +data base file. + +#+NAME: debug-repl +#+ATTR_LATEX: :width 12cm +#+CAPTION: Pamhyr2 debug Python REPL +[[./images/python-debug-repl.png]] + +* Build the project The project uses gitlab-ci runners to build packages, but it is possible to build packages manually. @@ -335,7 +1026,45 @@ winetricks installed. [fn:nsis] The NSIS web site: https://sourceforge.net/projects/nsis/ -** TODO Setup the CI environment +** Setup the CI environment + +Pamhyr2 need a Linux ci-runner and a Windows ci-runner for building +package. The windows ci-runner could run on a Wine environement. + +*** Linux + +The Linux ci-runner need some software and dependencies in addtion of +gitlab-ci. + +#+begin_src shell + sudo apt install \ + emacs emacs-goodies-el \ + texlive-full \ + python3.8 python3.8-venv + sudo python3 -m pip install pyinstaller +#+end_src + +*** Windows (Wine) + +The ci-runner environment for Wine need at least wine version 8, let +[[https://www.numetopia.fr/comment-installer-wine-sur-ubuntu-ou-linux-mint/][see who to add wine official depot to your linux distribution]]. + +#+begin_src shell +sudo apt install wine-stable winetricks +#+end_src + +In addition, the environment need windows version of: + - [[https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe][Python 3.8.10]] + - Git + - PowerShell + - Gitlab-ci + - [[https://freefr.dl.sourceforge.net/project/nsis/NSIS%203/3.08/nsis-3.08-setup.exe][Nsis]] + +Now, we can install =pyinstaller= on this windows environment: +#+begin_src shell +wine python -m pip install pyinstaller +#+end_src + * Documentation files This document and the user documentation are org files. This text file diff --git a/doc/dev/images/python-debug-repl.png b/doc/dev/images/python-debug-repl.png new file mode 100644 index 00000000..847f4afc Binary files /dev/null and b/doc/dev/images/python-debug-repl.png differ diff --git a/doc/users/Tuto1/Geo.png b/doc/users/Tuto1/Geo.png new file mode 100644 index 00000000..44a247db Binary files /dev/null and b/doc/users/Tuto1/Geo.png differ diff --git a/doc/users/Tuto1/K.png b/doc/users/Tuto1/K.png new file mode 100644 index 00000000..6b4cdbd6 Binary files /dev/null and b/doc/users/Tuto1/K.png differ diff --git a/doc/users/Tuto1/Logo-INRAE_Transparent.png b/doc/users/Tuto1/Logo-INRAE_Transparent.png new file mode 100644 index 00000000..0cd93142 Binary files /dev/null and b/doc/users/Tuto1/Logo-INRAE_Transparent.png differ diff --git a/doc/users/Tuto1/NEWSTUDY.png b/doc/users/Tuto1/NEWSTUDY.png new file mode 100644 index 00000000..a68b5e3a Binary files /dev/null and b/doc/users/Tuto1/NEWSTUDY.png differ diff --git a/doc/users/Tuto1/boundary.png b/doc/users/Tuto1/boundary.png new file mode 100644 index 00000000..4c28e80b Binary files /dev/null and b/doc/users/Tuto1/boundary.png differ diff --git a/doc/users/Tuto1/data/Bief_1.M b/doc/users/Tuto1/data/Bief_1.M new file mode 100644 index 00000000..19e85d6e --- /dev/null +++ b/doc/users/Tuto1/data/Bief_1.M @@ -0,0 +1,4488 @@ +# SECMA V 6.10 ENTREE: Bief_1.ST SORTIE: Bief_1.M +# Z: INTERPOL LINEAIRE +# D MOY SUR MAX DE BORD 1 -BORD 2 50.00 + 1 1 0 37 31090.0000 + 31090.0000 0.0000 23.7300 + 31090.0000 0.2083 23.4425 + 31090.0000 0.4167 23.1550 + 31090.0000 0.6250 22.8675 + 31090.0000 0.8333 22.5800 + 31090.0000 1.0417 22.2925 + 31090.0000 1.2500 22.0050 + 31090.0000 1.4583 21.7175 + 31090.0000 1.6667 21.4300 + 31090.0000 1.8750 21.1425 + 31090.0000 2.0833 20.8550 + 31090.0000 2.2917 20.5675 + 31090.0000 2.5000 20.2800 rg + 31090.0000 2.5000 19.4302 + 31090.0000 3.2095 19.2900 + 31090.0000 4.0593 19.2900 + 31090.0000 4.9085 19.3142 + 31090.0000 5.7573 19.3546 + 31090.0000 6.6061 19.3951 + 31090.0000 7.4549 19.4355 + 31090.0000 8.3037 19.4759 + 31090.0000 9.1525 19.5163 + 31090.0000 10.0013 19.5567 + 31090.0000 10.7000 19.7402 + 31090.0000 10.7000 20.5900 rd + 31090.0000 11.3124 20.7705 + 31090.0000 11.9249 20.9510 + 31090.0000 12.5373 21.1315 + 31090.0000 13.1497 21.3120 + 31090.0000 13.7621 21.4925 + 31090.0000 14.3746 21.6730 + 31090.0000 14.9254 21.9872 + 31090.0000 15.4603 22.3357 + 31090.0000 15.9952 22.6843 + 31090.0000 16.5301 23.0329 + 31090.0000 17.0651 23.3814 + 31090.0000 17.6000 23.7300 + 999.9990 999.9990 0.0000 + 2 1 1 37 31127.5000 + 31127.5000 0.0000 23.2850 + 31127.5000 0.2083 23.0283 + 31127.5000 0.4167 22.7717 + 31127.5000 0.6250 22.5150 + 31127.5000 0.8333 22.2583 + 31127.5000 1.0417 22.0017 + 31127.5000 1.2500 21.7450 + 31127.5000 1.4583 21.4883 + 31127.5000 1.6667 21.2317 + 31127.5000 1.8750 20.9750 + 31127.5000 2.0833 20.7183 + 31127.5000 2.2917 20.4617 + 31127.5000 2.5000 20.2050 rg + 31127.5000 2.5000 19.3590 + 31127.5000 3.2470 19.2600 + 31127.5000 4.0930 19.2600 + 31127.5000 4.9384 19.2856 + 31127.5000 5.7835 19.3259 + 31127.5000 6.6285 19.3661 + 31127.5000 7.4736 19.4064 + 31127.5000 8.3187 19.4466 + 31127.5000 9.1637 19.4868 + 31127.5000 10.0088 19.5271 + 31127.5000 10.7000 19.7140 + 31127.5000 10.7000 20.5600 rd + 31127.5000 11.3058 20.7464 + 31127.5000 11.9115 20.9329 + 31127.5000 12.5173 21.1193 + 31127.5000 13.1231 21.3057 + 31127.5000 13.7289 21.4921 + 31127.5000 14.3346 21.6786 + 31127.5000 14.8928 21.9703 + 31127.5000 15.4342 22.2992 + 31127.5000 15.9757 22.6282 + 31127.5000 16.5171 22.9571 + 31127.5000 17.0586 23.2861 + 31127.5000 17.6000 23.6150 + 999.9990 999.9990 0.0000 + 3 2 0 37 31165.0000 + 31165.0000 0.0000 22.8400 + 31165.0000 0.2083 22.6142 + 31165.0000 0.4167 22.3883 + 31165.0000 0.6250 22.1625 + 31165.0000 0.8333 21.9367 + 31165.0000 1.0417 21.7108 + 31165.0000 1.2500 21.4850 + 31165.0000 1.4583 21.2592 + 31165.0000 1.6667 21.0333 + 31165.0000 1.8750 20.8075 + 31165.0000 2.0833 20.5817 + 31165.0000 2.2917 20.3558 + 31165.0000 2.5000 20.1300 rg + 31165.0000 2.5000 19.2877 + 31165.0000 3.2845 19.2300 + 31165.0000 4.1268 19.2300 + 31165.0000 4.9684 19.2571 + 31165.0000 5.8097 19.2971 + 31165.0000 6.6510 19.3372 + 31165.0000 7.4923 19.3773 + 31165.0000 8.3336 19.4173 + 31165.0000 9.1749 19.4574 + 31165.0000 10.0163 19.4974 + 31165.0000 10.7000 19.6877 + 31165.0000 10.7000 20.5300 rd + 31165.0000 11.2991 20.7223 + 31165.0000 11.8982 20.9147 + 31165.0000 12.4974 21.1070 + 31165.0000 13.0965 21.2994 + 31165.0000 13.6956 21.4917 + 31165.0000 14.2947 21.6841 + 31165.0000 14.8602 21.9533 + 31165.0000 15.4082 22.2627 + 31165.0000 15.9561 22.5720 + 31165.0000 16.5041 22.8813 + 31165.0000 17.0520 23.1907 + 31165.0000 17.6000 23.5000 + 999.9990 999.9990 0.0000 + 4 2 1 37 31217.0000 + 31217.0000 -0.2000 22.8320 + 31217.0000 0.0117 22.6108 + 31217.0000 0.2235 22.3895 + 31217.0000 0.4352 22.1683 + 31217.0000 0.6470 21.9470 + 31217.0000 0.8587 21.7257 + 31217.0000 1.0703 21.5043 + 31217.0000 1.2819 21.2829 + 31217.0000 1.4935 21.0615 + 31217.0000 1.7051 20.8402 + 31217.0000 1.9168 20.6188 + 31217.0000 2.1284 20.3974 + 31217.0000 2.3400 20.1760 rg + 31217.0000 2.4741 19.3798 + 31217.0000 3.1540 19.1900 + 31217.0000 4.0093 19.1900 + 31217.0000 4.8641 19.2121 + 31217.0000 5.7185 19.2528 + 31217.0000 6.5728 19.2935 + 31217.0000 7.4272 19.3342 + 31217.0000 8.2815 19.3748 + 31217.0000 9.1358 19.4155 + 31217.0000 9.9902 19.4562 + 31217.0000 10.7000 19.6347 + 31217.0000 10.7000 20.4900 rd + 31217.0000 11.2956 20.6925 + 31217.0000 11.8912 20.8951 + 31217.0000 12.4867 21.0976 + 31217.0000 13.0823 21.3001 + 31217.0000 13.6779 21.5027 + 31217.0000 14.2735 21.7052 + 31217.0000 14.8408 21.9722 + 31217.0000 15.3927 22.2745 + 31217.0000 15.9445 22.5769 + 31217.0000 16.4963 22.8793 + 31217.0000 17.0482 23.1816 + 31217.0000 17.6000 23.4840 + 999.9990 999.9990 0.0000 + 5 2 2 37 31269.0000 + 31269.0000 -0.4000 22.8240 + 31269.0000 -0.1848 22.6074 + 31269.0000 0.0303 22.3907 + 31269.0000 0.2455 22.1741 + 31269.0000 0.4607 21.9574 + 31269.0000 0.6757 21.7406 + 31269.0000 0.8906 21.5236 + 31269.0000 1.1055 21.3067 + 31269.0000 1.3204 21.0898 + 31269.0000 1.5353 20.8728 + 31269.0000 1.7502 20.6559 + 31269.0000 1.9651 20.4389 + 31269.0000 2.1800 20.2220 rg + 31269.0000 2.4481 19.4720 + 31269.0000 3.0235 19.1500 + 31269.0000 3.8919 19.1500 + 31269.0000 4.7598 19.1671 + 31269.0000 5.6272 19.2084 + 31269.0000 6.4946 19.2497 + 31269.0000 7.3620 19.2910 + 31269.0000 8.2294 19.3324 + 31269.0000 9.0967 19.3737 + 31269.0000 9.9641 19.4150 + 31269.0000 10.7000 19.5816 + 31269.0000 10.7000 20.4500 rd + 31269.0000 11.2920 20.6627 + 31269.0000 11.8841 20.8754 + 31269.0000 12.4761 21.0882 + 31269.0000 13.0681 21.3009 + 31269.0000 13.6602 21.5136 + 31269.0000 14.2522 21.7263 + 31269.0000 14.8215 21.9910 + 31269.0000 15.3772 22.2864 + 31269.0000 15.9329 22.5818 + 31269.0000 16.4886 22.8772 + 31269.0000 17.0443 23.1726 + 31269.0000 17.6000 23.4680 + 999.9990 999.9990 0.0000 + 6 2 3 37 31321.0000 + 31321.0000 -0.6000 22.8160 + 31321.0000 -0.3814 22.6039 + 31321.0000 -0.1628 22.3919 + 31321.0000 0.0557 22.1798 + 31321.0000 0.2743 21.9678 + 31321.0000 0.4927 21.7555 + 31321.0000 0.7109 21.5430 + 31321.0000 0.9291 21.3305 + 31321.0000 1.1473 21.1180 + 31321.0000 1.3654 20.9055 + 31321.0000 1.5836 20.6930 + 31321.0000 1.8018 20.4805 + 31321.0000 2.0200 20.2680 rg + 31321.0000 2.4222 19.5641 + 31321.0000 2.8930 19.1100 + 31321.0000 3.7744 19.1100 + 31321.0000 4.6556 19.1222 + 31321.0000 5.5360 19.1641 + 31321.0000 6.4164 19.2060 + 31321.0000 7.2968 19.2479 + 31321.0000 8.1772 19.2899 + 31321.0000 9.0576 19.3318 + 31321.0000 9.9380 19.3737 + 31321.0000 10.7000 19.5286 + 31321.0000 10.7000 20.4100 rd + 31321.0000 11.2885 20.6329 + 31321.0000 11.8770 20.8558 + 31321.0000 12.4655 21.0787 + 31321.0000 13.0540 21.3016 + 31321.0000 13.6425 21.5245 + 31321.0000 14.2310 21.7474 + 31321.0000 14.8021 22.0098 + 31321.0000 15.3617 22.2982 + 31321.0000 15.9213 22.5867 + 31321.0000 16.4808 22.8751 + 31321.0000 17.0404 23.1636 + 31321.0000 17.6000 23.4520 + 999.9990 999.9990 0.0000 + 7 2 4 37 31373.0000 + 31373.0000 -0.8000 22.8080 + 31373.0000 -0.5780 22.6005 + 31373.0000 -0.3560 22.3931 + 31373.0000 -0.1340 22.1856 + 31373.0000 0.0880 21.9781 + 31373.0000 0.3097 21.7703 + 31373.0000 0.5312 21.5623 + 31373.0000 0.7526 21.3542 + 31373.0000 0.9741 21.1462 + 31373.0000 1.1956 20.9381 + 31373.0000 1.4171 20.7301 + 31373.0000 1.6385 20.5220 + 31373.0000 1.8600 20.3140 rg + 31373.0000 2.3963 19.6562 + 31373.0000 2.7625 19.0700 + 31373.0000 3.6570 19.0700 + 31373.0000 4.5513 19.0772 + 31373.0000 5.4447 19.1197 + 31373.0000 6.3382 19.1623 + 31373.0000 7.2316 19.2048 + 31373.0000 8.1251 19.2474 + 31373.0000 9.0185 19.2899 + 31373.0000 9.9120 19.3325 + 31373.0000 10.7000 19.4755 + 31373.0000 10.7000 20.3700 rd + 31373.0000 11.2850 20.6031 + 31373.0000 11.8699 20.8362 + 31373.0000 12.4549 21.0693 + 31373.0000 13.0398 21.3024 + 31373.0000 13.6248 21.5354 + 31373.0000 14.2097 21.7685 + 31373.0000 14.7827 22.0286 + 31373.0000 15.3462 22.3101 + 31373.0000 15.9096 22.5916 + 31373.0000 16.4731 22.8730 + 31373.0000 17.0365 23.1545 + 31373.0000 17.6000 23.4360 + 999.9990 999.9990 0.0000 + 8 3 0 37 31425.0000 + 31425.0000 -1.0000 22.8000 + 31425.0000 -0.7746 22.5971 + 31425.0000 -0.5492 22.3943 + 31425.0000 -0.3238 22.1914 + 31425.0000 -0.0983 21.9885 + 31425.0000 0.1267 21.7852 + 31425.0000 0.3515 21.5816 + 31425.0000 0.5762 21.3780 + 31425.0000 0.8010 21.1744 + 31425.0000 1.0257 20.9708 + 31425.0000 1.2505 20.7672 + 31425.0000 1.4752 20.5636 + 31425.0000 1.7000 20.3600 rg + 31425.0000 2.3704 19.7483 + 31425.0000 2.6320 19.0300 + 31425.0000 3.5395 19.0300 + 31425.0000 4.4470 19.0322 + 31425.0000 5.3535 19.0754 + 31425.0000 6.2600 19.1186 + 31425.0000 7.1664 19.1617 + 31425.0000 8.0729 19.2049 + 31425.0000 8.9794 19.2481 + 31425.0000 9.8859 19.2912 + 31425.0000 10.7000 19.4225 + 31425.0000 10.7000 20.3300 rd + 31425.0000 11.2814 20.5733 + 31425.0000 11.8628 20.8165 + 31425.0000 12.4442 21.0598 + 31425.0000 13.0256 21.3031 + 31425.0000 13.6071 21.5464 + 31425.0000 14.1885 21.7896 + 31425.0000 14.7633 22.0474 + 31425.0000 15.3307 22.3219 + 31425.0000 15.8980 22.5965 + 31425.0000 16.4653 22.8710 + 31425.0000 17.0327 23.1455 + 31425.0000 17.6000 23.4200 + 999.9990 999.9990 0.0000 + 9 3 1 37 31480.0000 + 31480.0000 -1.0000 22.7833 + 31480.0000 -0.7656 22.5892 + 31480.0000 -0.5311 22.3950 + 31480.0000 -0.2967 22.2009 + 31480.0000 -0.0629 22.0060 + 31480.0000 0.1564 21.7946 + 31480.0000 0.3755 21.5830 + 31480.0000 0.5946 21.3714 + 31480.0000 0.8137 21.1598 + 31480.0000 1.0328 20.9482 + 31480.0000 1.2518 20.7366 + 31480.0000 1.4709 20.5249 + 31480.0000 1.6900 20.3133 rg + 31480.0000 2.3400 19.6829 + 31480.0000 2.6435 18.9867 + 31480.0000 3.5499 18.9867 + 31480.0000 4.4562 18.9893 + 31480.0000 5.3615 19.0325 + 31480.0000 6.2668 19.0756 + 31480.0000 7.1722 19.1187 + 31480.0000 8.0775 19.1618 + 31480.0000 8.9829 19.2049 + 31480.0000 9.8882 19.2480 + 31480.0000 10.7000 19.3803 + 31480.0000 10.7000 20.2867 rd + 31480.0000 11.2767 20.5394 + 31480.0000 11.8534 20.7922 + 31480.0000 12.4301 21.0449 + 31480.0000 13.0069 21.2977 + 31480.0000 13.5836 21.5504 + 31480.0000 14.1603 21.8032 + 31480.0000 14.7344 22.0616 + 31480.0000 15.3075 22.3220 + 31480.0000 15.8806 22.5823 + 31480.0000 16.4538 22.8427 + 31480.0000 17.0269 23.1030 + 31480.0000 17.6000 23.3633 + 999.9990 999.9990 0.0000 + 10 3 2 37 31535.0000 + 31535.0000 -1.0000 22.7667 + 31535.0000 -0.7565 22.5812 + 31535.0000 -0.5131 22.3958 + 31535.0000 -0.2696 22.2103 + 31535.0000 -0.0274 22.0236 + 31535.0000 0.1861 21.8040 + 31535.0000 0.3995 21.5844 + 31535.0000 0.6130 21.3648 + 31535.0000 0.8264 21.1452 + 31535.0000 1.0398 20.9255 + 31535.0000 1.2532 20.7059 + 31535.0000 1.4666 20.4863 + 31535.0000 1.6800 20.2667 rg + 31535.0000 2.3096 19.6175 + 31535.0000 2.6550 18.9433 + 31535.0000 3.5602 18.9433 + 31535.0000 4.4653 18.9464 + 31535.0000 5.3695 18.9895 + 31535.0000 6.2737 19.0326 + 31535.0000 7.1779 19.0756 + 31535.0000 8.0821 19.1187 + 31535.0000 8.9863 19.1617 + 31535.0000 9.8905 19.2048 + 31535.0000 10.7000 19.3381 + 31535.0000 10.7000 20.2433 rd + 31535.0000 11.2720 20.5056 + 31535.0000 11.8440 20.7678 + 31535.0000 12.4161 21.0300 + 31535.0000 12.9881 21.2923 + 31535.0000 13.5601 21.5545 + 31535.0000 14.1321 21.8168 + 31535.0000 14.7055 22.0758 + 31535.0000 15.2844 22.3220 + 31535.0000 15.8633 22.5682 + 31535.0000 16.4422 22.8143 + 31535.0000 17.0211 23.0605 + 31535.0000 17.6000 23.3067 + 999.9990 999.9990 0.0000 + 11 4 0 37 31590.0000 + 31590.0000 -1.0000 22.7500 + 31590.0000 -0.7475 22.5733 + 31590.0000 -0.4951 22.3965 + 31590.0000 -0.2426 22.2198 + 31590.0000 0.0081 22.0411 + 31590.0000 0.2159 21.8135 + 31590.0000 0.4236 21.5858 + 31590.0000 0.6313 21.3582 + 31590.0000 0.8391 21.1305 + 31590.0000 1.0468 20.9029 + 31590.0000 1.2545 20.6753 + 31590.0000 1.4623 20.4476 + 31590.0000 1.6700 20.2200 rg + 31590.0000 2.2792 19.5520 + 31590.0000 2.6665 18.9000 + 31590.0000 3.5705 18.9000 + 31590.0000 4.4745 18.9035 + 31590.0000 5.3776 18.9466 + 31590.0000 6.2806 18.9896 + 31590.0000 7.1836 19.0326 + 31590.0000 8.0867 19.0756 + 31590.0000 8.9897 19.1186 + 31590.0000 9.8928 19.1616 + 31590.0000 10.7000 19.2959 + 31590.0000 10.7000 20.2000 rd + 31590.0000 11.2673 20.4717 + 31590.0000 11.8347 20.7434 + 31590.0000 12.4020 21.0152 + 31590.0000 12.9693 21.2869 + 31590.0000 13.5366 21.5586 + 31590.0000 14.1040 21.8303 + 31590.0000 14.6765 22.0900 + 31590.0000 15.2612 22.3220 + 31590.0000 15.8459 22.5540 + 31590.0000 16.4306 22.7860 + 31590.0000 17.0153 23.0180 + 31590.0000 17.6000 23.2500 + 999.9990 999.9990 0.0000 + 12 4 1 37 31637.5000 + 31637.5000 -1.0000 22.7375 + 31637.5000 -0.7451 22.5623 + 31637.5000 -0.4902 22.3872 + 31637.5000 -0.2352 22.2120 + 31637.5000 0.0152 22.0319 + 31637.5000 0.2199 21.8001 + 31637.5000 0.4246 21.5683 + 31637.5000 0.6292 21.3365 + 31637.5000 0.8339 21.1047 + 31637.5000 1.0385 20.8729 + 31637.5000 1.2432 20.6411 + 31637.5000 1.4478 20.4093 + 31637.5000 1.6525 20.1775 rg + 31637.5000 2.2515 19.5013 + 31637.5000 2.6712 18.8625 + 31637.5000 3.5748 18.8625 + 31637.5000 4.4783 18.8662 + 31637.5000 5.3809 18.9092 + 31637.5000 6.2834 18.9522 + 31637.5000 7.1860 18.9952 + 31637.5000 8.0886 19.0381 + 31637.5000 8.9912 19.0811 + 31637.5000 9.8937 19.1241 + 31637.5000 10.7000 19.2589 + 31637.5000 10.7000 20.1625 rd + 31637.5000 11.2634 20.4421 + 31637.5000 11.8268 20.7218 + 31637.5000 12.3902 21.0014 + 31637.5000 12.9535 21.2810 + 31637.5000 13.5169 21.5607 + 31637.5000 14.0803 21.8403 + 31637.5000 14.6492 22.1072 + 31637.5000 15.2393 22.3232 + 31637.5000 15.8295 22.5393 + 31637.5000 16.4197 22.7554 + 31637.5000 17.0098 22.9714 + 31637.5000 17.6000 23.1875 + 999.9990 999.9990 0.0000 + 13 4 2 37 31685.0000 + 31685.0000 -1.0000 22.7250 + 31685.0000 -0.7426 22.5514 + 31685.0000 -0.4852 22.3778 + 31685.0000 -0.2279 22.2042 + 31685.0000 0.0224 22.0227 + 31685.0000 0.2240 21.7867 + 31685.0000 0.4255 21.5507 + 31685.0000 0.6271 21.3148 + 31685.0000 0.8287 21.0788 + 31685.0000 1.0303 20.8429 + 31685.0000 1.2318 20.6069 + 31685.0000 1.4334 20.3710 + 31685.0000 1.6350 20.1350 rg + 31685.0000 2.2237 19.4506 + 31685.0000 2.6759 18.8250 + 31685.0000 3.5790 18.8250 + 31685.0000 4.4821 18.8289 + 31685.0000 5.3842 18.8719 + 31685.0000 6.2863 18.9148 + 31685.0000 7.1884 18.9578 + 31685.0000 8.0905 19.0007 + 31685.0000 8.9926 19.0437 + 31685.0000 9.8947 19.0867 + 31685.0000 10.7000 19.2219 + 31685.0000 10.7000 20.1250 rd + 31685.0000 11.2594 20.4125 + 31685.0000 11.8189 20.7001 + 31685.0000 12.3783 20.9876 + 31685.0000 12.9378 21.2752 + 31685.0000 13.4972 21.5627 + 31685.0000 14.0567 21.8502 + 31685.0000 14.6218 22.1243 + 31685.0000 15.2174 22.3245 + 31685.0000 15.8131 22.5246 + 31685.0000 16.4087 22.7247 + 31685.0000 17.0044 22.9249 + 31685.0000 17.6000 23.1250 + 999.9990 999.9990 0.0000 + 14 4 3 37 31732.5000 + 31732.5000 -1.0000 22.7125 + 31732.5000 -0.7402 22.5405 + 31732.5000 -0.4803 22.3684 + 31732.5000 -0.2205 22.1964 + 31732.5000 0.0295 22.0134 + 31732.5000 0.2280 21.7733 + 31732.5000 0.4265 21.5332 + 31732.5000 0.6250 21.2931 + 31732.5000 0.8235 21.0530 + 31732.5000 1.0220 20.8129 + 31732.5000 1.2205 20.5727 + 31732.5000 1.4190 20.3326 + 31732.5000 1.6175 20.0925 rg + 31732.5000 2.1960 19.3999 + 31732.5000 2.6806 18.7875 + 31732.5000 3.5833 18.7875 + 31732.5000 4.4858 18.7916 + 31732.5000 5.3875 18.8345 + 31732.5000 6.2891 18.8775 + 31732.5000 7.1907 18.9204 + 31732.5000 8.0923 18.9633 + 31732.5000 8.9940 19.0063 + 31732.5000 9.8956 19.0492 + 31732.5000 10.7000 19.1848 + 31732.5000 10.7000 20.0875 rd + 31732.5000 11.2555 20.3830 + 31732.5000 11.8110 20.6784 + 31732.5000 12.3665 20.9739 + 31732.5000 12.9220 21.2693 + 31732.5000 13.4775 21.5648 + 31732.5000 14.0331 21.8602 + 31732.5000 14.5944 22.1415 + 31732.5000 15.1956 22.3257 + 31732.5000 15.7967 22.5099 + 31732.5000 16.3978 22.6941 + 31732.5000 16.9989 22.8783 + 31732.5000 17.6000 23.0625 + 999.9990 999.9990 0.0000 + 15 5 0 37 31780.0000 + 31780.0000 -1.0000 22.7000 + 31780.0000 -0.7377 22.5295 + 31780.0000 -0.4754 22.3590 + 31780.0000 -0.2131 22.1885 + 31780.0000 0.0366 22.0042 + 31780.0000 0.2320 21.7599 + 31780.0000 0.4275 21.5157 + 31780.0000 0.6229 21.2714 + 31780.0000 0.8183 21.0271 + 31780.0000 1.0137 20.7828 + 31780.0000 1.2092 20.5386 + 31780.0000 1.4046 20.2943 + 31780.0000 1.6000 20.0500 rg + 31780.0000 2.1682 19.3492 + 31780.0000 2.6853 18.7500 + 31780.0000 3.5875 18.7500 + 31780.0000 4.4896 18.7543 + 31780.0000 5.3908 18.7972 + 31780.0000 6.2919 18.8401 + 31780.0000 7.1931 18.8830 + 31780.0000 8.0942 18.9259 + 31780.0000 8.9954 18.9688 + 31780.0000 9.8966 19.0117 + 31780.0000 10.7000 19.1478 + 31780.0000 10.7000 20.0500 rd + 31780.0000 11.2516 20.3534 + 31780.0000 11.8031 20.6567 + 31780.0000 12.3547 20.9601 + 31780.0000 12.9063 21.2635 + 31780.0000 13.4579 21.5668 + 31780.0000 14.0094 21.8702 + 31780.0000 14.5671 22.1586 + 31780.0000 15.1737 22.3269 + 31780.0000 15.7802 22.4952 + 31780.0000 16.3868 22.6634 + 31780.0000 16.9934 22.8317 + 31780.0000 17.6000 23.0000 + 999.9990 999.9990 0.0000 + 16 5 1 37 31827.0000 + 31827.0000 -0.5000 22.8450 + 31827.0000 -0.3527 22.5904 + 31827.0000 -0.1094 22.3705 + 31827.0000 0.1339 22.1507 + 31827.0000 0.3710 21.9239 + 31827.0000 0.5808 21.6672 + 31827.0000 0.7907 21.4104 + 31827.0000 1.0006 21.1537 + 31827.0000 1.2105 20.8970 + 31827.0000 1.4204 20.6402 + 31827.0000 1.6302 20.3835 + 31827.0000 1.8401 20.1267 + 31827.0000 2.0500 19.8700 rg + 31827.0000 2.3341 19.0943 + 31827.0000 2.9433 18.7200 + 31827.0000 3.8197 18.7200 + 31827.0000 4.6957 18.7341 + 31827.0000 5.5711 18.7758 + 31827.0000 6.4465 18.8175 + 31827.0000 7.3219 18.8591 + 31827.0000 8.1973 18.9008 + 31827.0000 9.0727 18.9425 + 31827.0000 9.9481 18.9842 + 31827.0000 10.7000 19.1436 + 31827.0000 10.7000 20.0200 rd + 31827.0000 11.2496 20.3302 + 31827.0000 11.7991 20.6404 + 31827.0000 12.3487 20.9506 + 31827.0000 12.8983 21.2607 + 31827.0000 13.4478 21.5709 + 31827.0000 13.9974 21.8811 + 31827.0000 14.5520 22.1791 + 31827.0000 15.1616 22.3422 + 31827.0000 15.7712 22.5054 + 31827.0000 16.3808 22.6686 + 31827.0000 16.9904 22.8318 + 31827.0000 17.6000 22.9950 + 999.9990 999.9990 0.0000 + 17 6 0 37 31874.0000 + 31874.0000 0.0000 22.9900 + 31874.0000 0.0323 22.6513 + 31874.0000 0.2566 22.3821 + 31874.0000 0.4810 22.1128 + 31874.0000 0.7053 21.8436 + 31874.0000 0.9296 21.5744 + 31874.0000 1.1540 21.3052 + 31874.0000 1.3783 21.0360 + 31874.0000 1.6026 20.7668 + 31874.0000 1.8270 20.4976 + 31874.0000 2.0513 20.2284 + 31874.0000 2.2757 19.9592 + 31874.0000 2.5000 19.6900 rg + 31874.0000 2.5000 18.8394 + 31874.0000 3.2012 18.6900 + 31874.0000 4.0518 18.6900 + 31874.0000 4.9018 18.7139 + 31874.0000 5.7514 18.7544 + 31874.0000 6.6011 18.7948 + 31874.0000 7.4507 18.8353 + 31874.0000 8.3003 18.8757 + 31874.0000 9.1500 18.9162 + 31874.0000 9.9996 18.9566 + 31874.0000 10.7000 19.1394 + 31874.0000 10.7000 19.9900 rd + 31874.0000 11.2476 20.3070 + 31874.0000 11.7951 20.6240 + 31874.0000 12.3427 20.9410 + 31874.0000 12.8902 21.2580 + 31874.0000 13.4378 21.5750 + 31874.0000 13.9854 21.8920 + 31874.0000 14.5368 22.1995 + 31874.0000 15.1495 22.3576 + 31874.0000 15.7621 22.5157 + 31874.0000 16.3747 22.6738 + 31874.0000 16.9874 22.8319 + 31874.0000 17.6000 22.9900 + 999.9990 999.9990 0.0000 + 18 7 0 37 31875.0000 PontRD101m + 31875.0000 0.5400 21.8900 + 31875.0000 0.7033 21.7067 + 31875.0000 0.8667 21.5233 + 31875.0000 1.0300 21.3400 + 31875.0000 1.1933 21.1567 + 31875.0000 1.3567 20.9733 + 31875.0000 1.5200 20.7900 + 31875.0000 1.6833 20.6067 + 31875.0000 1.8467 20.4233 + 31875.0000 2.0100 20.2400 + 31875.0000 2.1733 20.0567 + 31875.0000 2.3367 19.8733 + 31875.0000 2.5000 19.6900 rg + 31875.0000 2.5000 18.8394 + 31875.0000 3.2012 18.6900 + 31875.0000 4.0518 18.6900 + 31875.0000 4.9018 18.7139 + 31875.0000 5.7514 18.7544 + 31875.0000 6.6011 18.7948 + 31875.0000 7.4507 18.8353 + 31875.0000 8.3003 18.8757 + 31875.0000 9.1500 18.9162 + 31875.0000 9.9996 18.9566 + 31875.0000 10.7000 19.1394 + 31875.0000 10.7000 19.9900 rd + 31875.0000 10.9658 20.1483 + 31875.0000 11.2317 20.3067 + 31875.0000 11.4975 20.4650 + 31875.0000 11.7633 20.6233 + 31875.0000 12.0292 20.7817 + 31875.0000 12.2950 20.9400 + 31875.0000 12.5608 21.0983 + 31875.0000 12.8267 21.2567 + 31875.0000 13.0925 21.4150 + 31875.0000 13.3583 21.5733 + 31875.0000 13.6242 21.7317 + 31875.0000 13.8900 21.8900 + 999.9990 999.9990 0.0000 + 19 8 0 37 31885.0000 PontRD101v + 31885.0000 0.5400 21.8900 + 31885.0000 0.7033 21.7067 + 31885.0000 0.8667 21.5233 + 31885.0000 1.0300 21.3400 + 31885.0000 1.1933 21.1567 + 31885.0000 1.3567 20.9733 + 31885.0000 1.5200 20.7900 + 31885.0000 1.6833 20.6067 + 31885.0000 1.8467 20.4233 + 31885.0000 2.0100 20.2400 + 31885.0000 2.1733 20.0567 + 31885.0000 2.3367 19.8733 + 31885.0000 2.5000 19.6900 rg + 31885.0000 2.5000 18.8394 + 31885.0000 3.2012 18.6900 + 31885.0000 4.0518 18.6900 + 31885.0000 4.9018 18.7139 + 31885.0000 5.7514 18.7544 + 31885.0000 6.6011 18.7948 + 31885.0000 7.4507 18.8353 + 31885.0000 8.3003 18.8757 + 31885.0000 9.1500 18.9162 + 31885.0000 9.9996 18.9566 + 31885.0000 10.7000 19.1394 + 31885.0000 10.7000 19.9900 rd + 31885.0000 10.9658 20.1483 + 31885.0000 11.2317 20.3067 + 31885.0000 11.4975 20.4650 + 31885.0000 11.7633 20.6233 + 31885.0000 12.0292 20.7817 + 31885.0000 12.2950 20.9400 + 31885.0000 12.5608 21.0983 + 31885.0000 12.8267 21.2567 + 31885.0000 13.0925 21.4150 + 31885.0000 13.3583 21.5733 + 31885.0000 13.6242 21.7317 + 31885.0000 13.8900 21.8900 + 999.9990 999.9990 0.0000 + 20 9 0 37 31886.0000 + 31886.0000 0.0000 22.9900 + 31886.0000 0.0323 22.6513 + 31886.0000 0.2566 22.3821 + 31886.0000 0.4810 22.1128 + 31886.0000 0.7053 21.8436 + 31886.0000 0.9296 21.5744 + 31886.0000 1.1540 21.3052 + 31886.0000 1.3783 21.0360 + 31886.0000 1.6026 20.7668 + 31886.0000 1.8270 20.4976 + 31886.0000 2.0513 20.2284 + 31886.0000 2.2757 19.9592 + 31886.0000 2.5000 19.6900 rg + 31886.0000 2.5000 18.8394 + 31886.0000 3.2012 18.6900 + 31886.0000 4.0518 18.6900 + 31886.0000 4.9018 18.7139 + 31886.0000 5.7514 18.7544 + 31886.0000 6.6011 18.7948 + 31886.0000 7.4507 18.8353 + 31886.0000 8.3003 18.8757 + 31886.0000 9.1500 18.9162 + 31886.0000 9.9996 18.9566 + 31886.0000 10.7000 19.1394 + 31886.0000 10.7000 19.9900 rd + 31886.0000 11.2476 20.3070 + 31886.0000 11.7951 20.6240 + 31886.0000 12.3427 20.9410 + 31886.0000 12.8902 21.2580 + 31886.0000 13.4378 21.5750 + 31886.0000 13.9854 21.8920 + 31886.0000 14.5368 22.1995 + 31886.0000 15.1495 22.3576 + 31886.0000 15.7621 22.5157 + 31886.0000 16.3747 22.6738 + 31886.0000 16.9874 22.8319 + 31886.0000 17.6000 22.9900 + 999.9990 999.9990 0.0000 + 21 9 1 37 31929.0000 + 31929.0000 0.0000 22.8733 + 31929.0000 0.0625 22.5615 + 31929.0000 0.2613 22.3017 + 31929.0000 0.4743 22.0512 + 31929.0000 0.6874 21.8008 + 31929.0000 0.9004 21.5504 + 31929.0000 1.1134 21.2999 + 31929.0000 1.3265 21.0495 + 31929.0000 1.5319 20.7934 + 31929.0000 1.7323 20.5333 + 31929.0000 1.9326 20.2733 + 31929.0000 2.1330 20.0133 + 31929.0000 2.3333 19.7533 rg + 31929.0000 2.5277 18.9491 + 31929.0000 3.1948 18.6533 + 31929.0000 4.0685 18.6533 + 31929.0000 4.9418 18.6693 + 31929.0000 5.8144 18.7070 + 31929.0000 6.6866 18.7564 + 31929.0000 7.5588 18.8058 + 31929.0000 8.4310 18.8552 + 31929.0000 9.3032 18.9046 + 31929.0000 10.1715 18.9800 + 31929.0000 10.9375 19.1697 + 31929.0000 10.9667 20.0200 rd + 31929.0000 11.5057 20.3270 + 31929.0000 12.0447 20.6341 + 31929.0000 12.5838 20.9411 + 31929.0000 13.1228 21.2481 + 31929.0000 13.6618 21.5552 + 31929.0000 14.2009 21.8622 + 31929.0000 14.7447 22.1566 + 31929.0000 15.3486 22.2972 + 31929.0000 15.9525 22.4378 + 31929.0000 16.5563 22.5784 + 31929.0000 17.1602 22.7189 + 31929.0000 17.7333 22.9300 + 999.9990 999.9990 0.0000 + 22 9 2 37 31972.0000 + 31972.0000 0.0000 22.7567 + 31972.0000 0.0926 22.4718 + 31972.0000 0.2660 22.2213 + 31972.0000 0.4677 21.9896 + 31972.0000 0.6695 21.7580 + 31972.0000 0.8712 21.5263 + 31972.0000 1.0729 21.2946 + 31972.0000 1.2746 21.0629 + 31972.0000 1.4613 20.8199 + 31972.0000 1.6376 20.5691 + 31972.0000 1.8140 20.3183 + 31972.0000 1.9903 20.0675 + 31972.0000 2.1667 19.8167 rg + 31972.0000 2.5554 19.0589 + 31972.0000 3.1885 18.6167 + 31972.0000 4.0852 18.6167 + 31972.0000 4.9818 18.6246 + 31972.0000 5.8774 18.6596 + 31972.0000 6.7722 18.7180 + 31972.0000 7.6669 18.7763 + 31972.0000 8.5617 18.8346 + 31972.0000 9.4565 18.8930 + 31972.0000 10.3435 19.0034 + 31972.0000 11.1749 19.1999 + 31972.0000 11.2333 20.0500 rd + 31972.0000 11.7638 20.3471 + 31972.0000 12.2944 20.6441 + 31972.0000 12.8249 20.9412 + 31972.0000 13.3554 21.2383 + 31972.0000 13.8859 21.5353 + 31972.0000 14.4164 21.8324 + 31972.0000 14.9526 22.1138 + 31972.0000 15.5477 22.2368 + 31972.0000 16.1428 22.3599 + 31972.0000 16.7379 22.4829 + 31972.0000 17.3330 22.6060 + 31972.0000 17.8667 22.8700 + 999.9990 999.9990 0.0000 + 23 10 0 37 32015.0000 + 32015.0000 0.0000 22.6400 + 32015.0000 0.1228 22.3821 + 32015.0000 0.2707 22.1409 + 32015.0000 0.4611 21.9280 + 32015.0000 0.6515 21.7151 + 32015.0000 0.8419 21.5022 + 32015.0000 1.0323 21.2893 + 32015.0000 1.2228 21.0764 + 32015.0000 1.3906 20.8464 + 32015.0000 1.5429 20.6048 + 32015.0000 1.6953 20.3632 + 32015.0000 1.8476 20.1216 + 32015.0000 2.0000 19.8800 rg + 32015.0000 2.5831 19.1686 + 32015.0000 3.1821 18.5800 + 32015.0000 4.1019 18.5800 + 32015.0000 5.0217 18.5800 + 32015.0000 5.9404 18.6123 + 32015.0000 6.8577 18.6796 + 32015.0000 7.7750 18.7468 + 32015.0000 8.6924 18.8141 + 32015.0000 9.6097 18.8814 + 32015.0000 10.5154 19.0268 + 32015.0000 11.4124 19.2301 + 32015.0000 11.5000 20.0800 rd + 32015.0000 12.0220 20.3671 + 32015.0000 12.5440 20.6542 + 32015.0000 13.0660 20.9413 + 32015.0000 13.5880 21.2284 + 32015.0000 14.1099 21.5155 + 32015.0000 14.6319 21.8026 + 32015.0000 15.1606 22.0709 + 32015.0000 15.7469 22.1764 + 32015.0000 16.3332 22.2820 + 32015.0000 16.9195 22.3875 + 32015.0000 17.5058 22.4930 + 32015.0000 18.0000 22.8100 + 999.9990 999.9990 0.0000 + 24 10 1 37 32062.5000 + 32062.5000 0.0000 22.5625 + 32062.5000 0.1515 22.3235 + 32062.5000 0.3193 22.0936 + 32062.5000 0.5189 21.8850 + 32062.5000 0.7185 21.6764 + 32062.5000 0.9181 21.4678 + 32062.5000 1.1177 21.2592 + 32062.5000 1.3124 21.0460 + 32062.5000 1.4842 20.8143 + 32062.5000 1.6444 20.5738 + 32062.5000 1.8046 20.3334 + 32062.5000 1.9648 20.0929 + 32062.5000 2.1250 19.8525 rg + 32062.5000 2.7098 19.1501 + 32062.5000 3.3018 18.5475 + 32062.5000 4.2159 18.5475 + 32062.5000 5.1299 18.5475 + 32062.5000 6.0431 18.5717 + 32062.5000 6.9513 18.6484 + 32062.5000 7.8530 18.7663 + 32062.5000 8.7562 18.8783 + 32062.5000 9.6662 18.9599 + 32062.5000 10.5675 19.1002 + 32062.5000 11.4623 19.2839 + 32062.5000 11.7500 19.9525 rd + 32062.5000 12.1415 20.3180 + 32062.5000 12.6640 20.6040 + 32062.5000 13.1887 20.8887 + 32062.5000 13.7133 21.1734 + 32062.5000 14.2380 21.4581 + 32062.5000 14.7626 21.7428 + 32062.5000 15.2922 22.0135 + 32062.5000 15.8758 22.1325 + 32062.5000 16.4622 22.2435 + 32062.5000 17.0487 22.3546 + 32062.5000 17.6351 22.4656 + 32062.5000 18.1500 22.7450 + 999.9990 999.9990 0.0000 + 25 10 2 37 32110.0000 + 32110.0000 0.0000 22.4850 + 32110.0000 0.1803 22.2648 + 32110.0000 0.3678 22.0463 + 32110.0000 0.5766 21.8420 + 32110.0000 0.7854 21.6377 + 32110.0000 0.9942 21.4334 + 32110.0000 1.2030 21.2291 + 32110.0000 1.4021 21.0156 + 32110.0000 1.5779 20.7822 + 32110.0000 1.7459 20.5429 + 32110.0000 1.9139 20.3036 + 32110.0000 2.0820 20.0643 + 32110.0000 2.2500 19.8250 rg + 32110.0000 2.8364 19.1315 + 32110.0000 3.4216 18.5150 + 32110.0000 4.3298 18.5150 + 32110.0000 5.2381 18.5150 + 32110.0000 6.1458 18.5311 + 32110.0000 7.0448 18.6172 + 32110.0000 7.9311 18.7859 + 32110.0000 8.8201 18.9424 + 32110.0000 9.7228 19.0385 + 32110.0000 10.6196 19.1736 + 32110.0000 11.5122 19.3377 + 32110.0000 12.0000 19.8250 rd + 32110.0000 12.2610 20.2688 + 32110.0000 12.7841 20.5537 + 32110.0000 13.3114 20.8361 + 32110.0000 13.8387 21.1184 + 32110.0000 14.3660 21.4007 + 32110.0000 14.8933 21.6831 + 32110.0000 15.4239 21.9561 + 32110.0000 16.0047 22.0885 + 32110.0000 16.5913 22.2051 + 32110.0000 17.1779 22.3216 + 32110.0000 17.7645 22.4382 + 32110.0000 18.3000 22.6800 + 999.9990 999.9990 0.0000 + 26 10 3 37 32157.5000 + 32157.5000 0.0000 22.4075 + 32157.5000 0.2090 22.2061 + 32157.5000 0.4164 21.9990 + 32157.5000 0.6343 21.7990 + 32157.5000 0.8523 21.5990 + 32157.5000 1.0703 21.3990 + 32157.5000 1.2883 21.1990 + 32157.5000 1.4918 20.9853 + 32157.5000 1.6715 20.7500 + 32157.5000 1.8474 20.5119 + 32157.5000 2.0233 20.2738 + 32157.5000 2.1991 20.0356 + 32157.5000 2.3750 19.7975 rg + 32157.5000 2.9631 19.1130 + 32157.5000 3.5413 18.4825 + 32157.5000 4.4438 18.4825 + 32157.5000 5.3463 18.4825 + 32157.5000 6.2486 18.4906 + 32157.5000 7.1384 18.5860 + 32157.5000 8.0091 18.8054 + 32157.5000 8.8839 19.0066 + 32157.5000 9.7793 19.1170 + 32157.5000 10.6718 19.2470 + 32157.5000 11.5621 19.3914 + 32157.5000 12.2500 19.6975 rd + 32157.5000 12.3805 20.2197 + 32157.5000 12.9042 20.5035 + 32157.5000 13.4341 20.7834 + 32157.5000 13.9641 21.0634 + 32157.5000 14.4940 21.3434 + 32157.5000 15.0239 21.6233 + 32157.5000 15.5555 21.8986 + 32157.5000 16.1336 22.0445 + 32157.5000 16.7204 22.1666 + 32157.5000 17.3071 22.2887 + 32157.5000 17.8939 22.4107 + 32157.5000 18.4500 22.6150 + 999.9990 999.9990 0.0000 + 27 11 0 37 32205.0000 + 32205.0000 0.0000 22.3300 + 32205.0000 0.2378 22.1475 + 32205.0000 0.4649 21.9518 + 32205.0000 0.6921 21.7560 + 32205.0000 0.9193 21.5603 + 32205.0000 1.1464 21.3646 + 32205.0000 1.3736 21.1689 + 32205.0000 1.5815 20.9549 + 32205.0000 1.7652 20.7179 + 32205.0000 1.9489 20.4809 + 32205.0000 2.1326 20.2440 + 32205.0000 2.3163 20.0070 + 32205.0000 2.5000 19.7700 rg + 32205.0000 3.0898 19.0944 + 32205.0000 3.6610 18.4500 + 32205.0000 4.5578 18.4500 + 32205.0000 5.4545 18.4500 + 32205.0000 6.3513 18.4500 + 32205.0000 7.2320 18.5548 + 32205.0000 8.0871 18.8249 + 32205.0000 8.9478 19.0708 + 32205.0000 9.8358 19.1956 + 32205.0000 10.7239 19.3204 + 32205.0000 11.6119 19.4452 + 32205.0000 12.5000 19.5700 rd + 32205.0000 12.5000 20.1706 + 32205.0000 13.0242 20.4532 + 32205.0000 13.5568 20.7308 + 32205.0000 14.0894 21.0084 + 32205.0000 14.6220 21.2860 + 32205.0000 15.1546 21.5636 + 32205.0000 15.6872 21.8412 + 32205.0000 16.2626 22.0006 + 32205.0000 16.8495 22.1281 + 32205.0000 17.4363 22.2557 + 32205.0000 18.0232 22.3833 + 32205.0000 18.6000 22.5500 + 999.9990 999.9990 0.0000 + 28 11 1 37 32256.0000 + 32256.0000 0.0000 22.2940 + 32256.0000 0.2560 22.1402 + 32256.0000 0.4941 21.9472 + 32256.0000 0.7321 21.7542 + 32256.0000 0.9702 21.5613 + 32256.0000 1.2082 21.3683 + 32256.0000 1.4462 21.1753 + 32256.0000 1.6574 20.9551 + 32256.0000 1.8459 20.7129 + 32256.0000 2.0344 20.4707 + 32256.0000 2.2230 20.2284 + 32256.0000 2.4115 19.9862 + 32256.0000 2.6000 19.7440 rg + 32256.0000 3.1711 19.1093 + 32256.0000 3.7273 18.4994 + 32256.0000 4.5447 18.4160 + 32256.0000 5.3990 18.4160 + 32256.0000 6.2533 18.4160 + 32256.0000 7.0947 18.4999 + 32256.0000 7.9157 18.7159 + 32256.0000 8.7411 18.9126 + 32256.0000 9.5885 19.0125 + 32256.0000 10.4207 19.1697 + 32256.0000 11.2504 19.3368 + 32256.0000 12.0800 19.5040 rd + 32256.0000 12.2390 20.0206 + 32256.0000 12.8173 20.2829 + 32256.0000 13.4024 20.5411 + 32256.0000 13.9874 20.7993 + 32256.0000 14.5020 21.1229 + 32256.0000 15.0776 21.4100 + 32256.0000 15.6531 21.6972 + 32256.0000 16.2629 21.8898 + 32256.0000 16.8841 22.0506 + 32256.0000 17.5109 22.1954 + 32256.0000 18.1378 22.3401 + 32256.0000 18.7400 22.5500 + 999.9990 999.9990 0.0000 + 29 11 2 37 32307.0000 + 32307.0000 0.0000 22.2580 + 32307.0000 0.2743 22.1330 + 32307.0000 0.5232 21.9427 + 32307.0000 0.7721 21.7524 + 32307.0000 1.0211 21.5622 + 32307.0000 1.2700 21.3719 + 32307.0000 1.5189 21.1817 + 32307.0000 1.7334 20.9553 + 32307.0000 1.9267 20.7078 + 32307.0000 2.1200 20.4604 + 32307.0000 2.3133 20.2129 + 32307.0000 2.5067 19.9655 + 32307.0000 2.7000 19.7180 rg + 32307.0000 3.2524 19.1241 + 32307.0000 3.7936 18.5489 + 32307.0000 4.5316 18.3820 + 32307.0000 5.3434 18.3820 + 32307.0000 6.1553 18.3820 + 32307.0000 6.9574 18.4449 + 32307.0000 7.7443 18.6069 + 32307.0000 8.5345 18.7545 + 32307.0000 9.3411 18.8293 + 32307.0000 10.1175 19.0190 + 32307.0000 10.8888 19.2285 + 32307.0000 11.6600 19.4380 rd + 32307.0000 11.9779 19.8706 + 32307.0000 12.6104 20.1125 + 32307.0000 13.2479 20.3513 + 32307.0000 13.8854 20.5901 + 32307.0000 14.3821 20.9597 + 32307.0000 15.0006 21.2565 + 32307.0000 15.6190 21.5532 + 32307.0000 16.2632 21.7790 + 32307.0000 16.9187 21.9731 + 32307.0000 17.5855 22.1350 + 32307.0000 18.2523 22.2970 + 32307.0000 18.8800 22.5500 + 999.9990 999.9990 0.0000 + 30 11 3 37 32358.0000 + 32358.0000 0.0000 22.2220 + 32358.0000 0.2926 22.1257 + 32358.0000 0.5524 21.9382 + 32358.0000 0.8122 21.7506 + 32358.0000 1.0720 21.5631 + 32358.0000 1.3318 21.3756 + 32358.0000 1.5916 21.1881 + 32358.0000 1.8093 20.9555 + 32358.0000 2.0074 20.7028 + 32358.0000 2.2056 20.4501 + 32358.0000 2.4037 20.1974 + 32358.0000 2.6019 19.9447 + 32358.0000 2.8000 19.6920 rg + 32358.0000 3.3336 19.1389 + 32358.0000 3.8599 18.5983 + 32358.0000 4.5185 18.3480 + 32358.0000 5.2879 18.3480 + 32358.0000 6.0573 18.3480 + 32358.0000 6.8202 18.3899 + 32358.0000 7.5729 18.4980 + 32358.0000 8.3278 18.5963 + 32358.0000 9.0937 18.6462 + 32358.0000 9.8144 18.8683 + 32358.0000 10.5272 19.1201 + 32358.0000 11.2400 19.3720 rd + 32358.0000 11.7169 19.7206 + 32358.0000 12.4035 19.9421 + 32358.0000 13.0934 20.1615 + 32358.0000 13.7834 20.3809 + 32358.0000 14.2621 20.7966 + 32358.0000 14.9235 21.1029 + 32358.0000 15.5850 21.4092 + 32358.0000 16.2635 21.6682 + 32358.0000 16.9533 21.8955 + 32358.0000 17.6601 22.0747 + 32358.0000 18.3668 22.2538 + 32358.0000 19.0200 22.5500 + 999.9990 999.9990 0.0000 + 31 11 4 37 32409.0000 + 32409.0000 0.0000 22.1860 + 32409.0000 0.3108 22.1184 + 32409.0000 0.5815 21.9336 + 32409.0000 0.8522 21.7488 + 32409.0000 1.1229 21.5640 + 32409.0000 1.3935 21.3792 + 32409.0000 1.6642 21.1944 + 32409.0000 1.8852 20.9557 + 32409.0000 2.0882 20.6977 + 32409.0000 2.2911 20.4398 + 32409.0000 2.4941 20.1819 + 32409.0000 2.6970 19.9239 + 32409.0000 2.9000 19.6660 rg + 32409.0000 3.4149 19.1538 + 32409.0000 3.9262 18.6477 + 32409.0000 4.5054 18.3140 + 32409.0000 5.2323 18.3140 + 32409.0000 5.9592 18.3140 + 32409.0000 6.6829 18.3350 + 32409.0000 7.4015 18.3890 + 32409.0000 8.1212 18.4382 + 32409.0000 8.8464 18.4631 + 32409.0000 9.5112 18.7176 + 32409.0000 10.1656 19.0118 + 32409.0000 10.8200 19.3060 rd + 32409.0000 11.4559 19.5706 + 32409.0000 12.1966 19.7717 + 32409.0000 12.9390 19.9717 + 32409.0000 13.6814 20.1718 + 32409.0000 14.1421 20.6334 + 32409.0000 14.8465 20.9493 + 32409.0000 15.5509 21.2652 + 32409.0000 16.2638 21.5574 + 32409.0000 16.9880 21.8180 + 32409.0000 17.7347 22.0143 + 32409.0000 18.4814 22.2106 + 32409.0000 19.1600 22.5500 + 999.9990 999.9990 0.0000 + 32 12 0 37 32460.0000 + 32460.0000 0.0000 22.1500 + 32460.0000 0.3291 22.1112 + 32460.0000 0.6106 21.9291 + 32460.0000 0.8922 21.7470 + 32460.0000 1.1737 21.5650 + 32460.0000 1.4553 21.3829 + 32460.0000 1.7369 21.2008 + 32460.0000 1.9612 20.9558 + 32460.0000 2.1689 20.6927 + 32460.0000 2.3767 20.4295 + 32460.0000 2.5845 20.1663 + 32460.0000 2.7922 19.9032 + 32460.0000 3.0000 19.6400 rg + 32460.0000 3.4962 19.1686 + 32460.0000 3.9924 18.6972 + 32460.0000 4.4923 18.2800 + 32460.0000 5.1768 18.2800 + 32460.0000 5.8612 18.2800 + 32460.0000 6.5456 18.2800 + 32460.0000 7.2301 18.2800 + 32460.0000 7.9145 18.2800 + 32460.0000 8.5990 18.2800 + 32460.0000 9.2080 18.5669 + 32460.0000 9.8040 18.9034 + 32460.0000 10.4000 19.2400 rd + 32460.0000 11.1948 19.4206 + 32460.0000 11.9897 19.6013 + 32460.0000 12.7845 19.7819 + 32460.0000 13.5794 19.9626 + 32460.0000 14.0222 20.4703 + 32460.0000 14.7695 20.7957 + 32460.0000 15.5168 21.1212 + 32460.0000 16.2641 21.4466 + 32460.0000 17.0226 21.7404 + 32460.0000 17.8092 21.9539 + 32460.0000 18.5959 22.1675 + 32460.0000 19.3000 22.5500 + 999.9990 999.9990 0.0000 + 33 12 1 37 32508.7500 + 32508.7500 0.0000 22.2250 + 32508.7500 0.3082 22.1222 + 32508.7500 0.6003 21.9372 + 32508.7500 0.8981 21.7593 + 32508.7500 1.1958 21.5814 + 32508.7500 1.4935 21.4035 + 32508.7500 1.7912 21.2256 + 32508.7500 2.0296 20.9803 + 32508.7500 2.2437 20.7067 + 32508.7500 2.4577 20.4332 + 32508.7500 2.6718 20.1596 + 32508.7500 2.8859 19.8861 + 32508.7500 3.1000 19.6125 rg + 32508.7500 3.6174 19.1346 + 32508.7500 4.1348 18.6566 + 32508.7500 4.6613 18.2450 + 32508.7500 5.3659 18.2450 + 32508.7500 6.0704 18.2450 + 32508.7500 6.7750 18.2450 + 32508.7500 7.4796 18.2450 + 32508.7500 8.1841 18.2450 + 32508.7500 8.8887 18.2450 + 32508.7500 9.5122 18.5278 + 32508.7500 10.1061 18.9027 + 32508.7500 10.7000 19.2775 rd + 32508.7500 11.4731 19.4655 + 32508.7500 12.2463 19.6535 + 32508.7500 13.0194 19.8416 + 32508.7500 13.7926 20.0296 + 32508.7500 14.2707 20.4910 + 32508.7500 15.0032 20.8021 + 32508.7500 15.7358 21.1131 + 32508.7500 16.4684 21.4242 + 32508.7500 17.2106 21.7077 + 32508.7500 17.9761 21.9251 + 32508.7500 18.7417 22.1426 + 32508.7500 19.4250 22.5250 + 999.9990 999.9990 0.0000 + 34 12 2 37 32557.5000 + 32557.5000 0.0000 22.3000 + 32557.5000 0.2873 22.1332 + 32557.5000 0.5901 21.9452 + 32557.5000 0.9039 21.7715 + 32557.5000 1.2178 21.5977 + 32557.5000 1.5316 21.4240 + 32557.5000 1.8455 21.2503 + 32557.5000 2.0980 21.0048 + 32557.5000 2.3184 20.7208 + 32557.5000 2.5388 20.4369 + 32557.5000 2.7592 20.1529 + 32557.5000 2.9796 19.8690 + 32557.5000 3.2000 19.5850 rg + 32557.5000 3.7386 19.1005 + 32557.5000 4.2772 18.6161 + 32557.5000 4.8303 18.2100 + 32557.5000 5.5550 18.2100 + 32557.5000 6.2797 18.2100 + 32557.5000 7.0044 18.2100 + 32557.5000 7.7290 18.2100 + 32557.5000 8.4537 18.2100 + 32557.5000 9.1784 18.2100 + 32557.5000 9.8164 18.4888 + 32557.5000 10.4082 18.9019 + 32557.5000 11.0000 19.3150 rd + 32557.5000 11.7515 19.5104 + 32557.5000 12.5029 19.7058 + 32557.5000 13.2544 19.9012 + 32557.5000 14.0058 20.0966 + 32557.5000 14.5192 20.5117 + 32557.5000 15.2370 20.8084 + 32557.5000 15.9548 21.1051 + 32557.5000 16.6726 21.4018 + 32557.5000 17.3986 21.6750 + 32557.5000 18.1430 21.8963 + 32557.5000 18.8874 22.1177 + 32557.5000 19.5500 22.5000 + 999.9990 999.9990 0.0000 + 35 12 3 37 32606.2500 + 32606.2500 0.0000 22.3750 + 32606.2500 0.2665 22.1443 + 32606.2500 0.5798 21.9532 + 32606.2500 0.9098 21.7837 + 32606.2500 1.2398 21.6141 + 32606.2500 1.5698 21.4445 + 32606.2500 1.8998 21.2750 + 32606.2500 2.1664 21.0292 + 32606.2500 2.3931 20.7349 + 32606.2500 2.6198 20.4405 + 32606.2500 2.8466 20.1462 + 32606.2500 3.0733 19.8518 + 32606.2500 3.3000 19.5575 rg + 32606.2500 3.8598 19.0665 + 32606.2500 4.4196 18.5755 + 32606.2500 4.9993 18.1750 + 32606.2500 5.7441 18.1750 + 32606.2500 6.4889 18.1750 + 32606.2500 7.2337 18.1750 + 32606.2500 7.9785 18.1750 + 32606.2500 8.7233 18.1750 + 32606.2500 9.4681 18.1750 + 32606.2500 10.1206 18.4497 + 32606.2500 10.7103 18.9011 + 32606.2500 11.3000 19.3525 rd + 32606.2500 12.0298 19.5553 + 32606.2500 12.7595 19.7580 + 32606.2500 13.4893 19.9608 + 32606.2500 14.2190 20.1635 + 32606.2500 14.7677 20.5324 + 32606.2500 15.4707 20.8147 + 32606.2500 16.1737 21.0970 + 32606.2500 16.8768 21.3793 + 32606.2500 17.5866 21.6423 + 32606.2500 18.3099 21.8675 + 32606.2500 19.0332 22.0928 + 32606.2500 19.6750 22.4750 + 999.9990 999.9990 0.0000 + 36 13 0 37 32655.0000 + 32655.0000 0.0000 22.4500 + 32655.0000 0.2456 22.1553 + 32655.0000 0.5695 21.9613 + 32655.0000 0.9156 21.7959 + 32655.0000 1.2618 21.6305 + 32655.0000 1.6079 21.4651 + 32655.0000 1.9541 21.2997 + 32655.0000 2.2348 21.0537 + 32655.0000 2.4678 20.7490 + 32655.0000 2.7009 20.4442 + 32655.0000 2.9339 20.1395 + 32655.0000 3.1670 19.8347 + 32655.0000 3.4000 19.5300 rg + 32655.0000 3.9810 19.0325 + 32655.0000 4.5620 18.5350 + 32655.0000 5.1683 18.1400 + 32655.0000 5.9333 18.1400 + 32655.0000 6.6982 18.1400 + 32655.0000 7.4631 18.1400 + 32655.0000 8.2280 18.1400 + 32655.0000 8.9929 18.1400 + 32655.0000 9.7578 18.1400 + 32655.0000 10.4248 18.4106 + 32655.0000 11.0124 18.9003 + 32655.0000 11.6000 19.3900 rd + 32655.0000 12.3081 19.6001 + 32655.0000 13.0161 19.8103 + 32655.0000 13.7242 20.0204 + 32655.0000 14.4322 20.2305 + 32655.0000 15.0162 20.5531 + 32655.0000 15.7044 20.8210 + 32655.0000 16.3927 21.0889 + 32655.0000 17.0810 21.3569 + 32655.0000 17.7747 21.6096 + 32655.0000 18.4768 21.8387 + 32655.0000 19.1790 22.0679 + 32655.0000 19.8000 22.4500 + 999.9990 999.9990 0.0000 + 37 13 1 37 32705.0000 + 32705.0000 0.0000 22.4500 + 32705.0000 0.2456 22.1553 + 32705.0000 0.5695 21.9613 + 32705.0000 0.9156 21.7959 + 32705.0000 1.2618 21.6305 + 32705.0000 1.6079 21.4651 + 32705.0000 1.9541 21.2997 + 32705.0000 2.2348 21.0537 + 32705.0000 2.4678 20.7490 + 32705.0000 2.7009 20.4442 + 32705.0000 2.9339 20.1395 + 32705.0000 3.1670 19.8347 + 32705.0000 3.4000 19.5300 rg + 32705.0000 3.8613 18.8600 + 32705.0000 4.3916 18.2881 + 32705.0000 5.1489 18.0750 + 32705.0000 5.9860 18.0750 + 32705.0000 6.8230 18.0750 + 32705.0000 7.6600 18.0750 + 32705.0000 8.4970 18.0750 + 32705.0000 9.3340 18.0750 + 32705.0000 10.1475 18.1790 + 32705.0000 10.8915 18.5097 + 32705.0000 11.5957 18.9498 + 32705.0000 12.3000 19.3900 rd + 32705.0000 12.9311 19.6450 + 32705.0000 13.5623 19.9001 + 32705.0000 14.1934 20.1551 + 32705.0000 14.7841 20.4151 + 32705.0000 15.3697 20.6907 + 32705.0000 16.0074 20.9390 + 32705.0000 16.6452 21.1872 + 32705.0000 17.2830 21.4355 + 32705.0000 17.9292 21.6601 + 32705.0000 18.5798 21.8724 + 32705.0000 19.2305 22.0847 + 32705.0000 19.8000 22.4500 + 999.9990 999.9990 0.0000 + 38 14 0 37 32755.0000 + 32755.0000 0.0000 22.4500 + 32755.0000 0.2456 22.1553 + 32755.0000 0.5695 21.9613 + 32755.0000 0.9156 21.7959 + 32755.0000 1.2618 21.6305 + 32755.0000 1.6079 21.4651 + 32755.0000 1.9541 21.2997 + 32755.0000 2.2348 21.0537 + 32755.0000 2.4678 20.7490 + 32755.0000 2.7009 20.4442 + 32755.0000 2.9339 20.1395 + 32755.0000 3.1670 19.8347 + 32755.0000 3.4000 19.5300 rg + 32755.0000 3.7416 18.6875 + 32755.0000 4.2211 18.0412 + 32755.0000 5.1296 18.0100 + 32755.0000 6.0387 18.0100 + 32755.0000 6.9478 18.0100 + 32755.0000 7.8569 18.0100 + 32755.0000 8.7660 18.0100 + 32755.0000 9.6751 18.0100 + 32755.0000 10.5373 18.2181 + 32755.0000 11.3582 18.6087 + 32755.0000 12.1791 18.9994 + 32755.0000 13.0000 19.3900 rd + 32755.0000 13.5542 19.6899 + 32755.0000 14.1085 19.9899 + 32755.0000 14.6627 20.2898 + 32755.0000 15.1359 20.5997 + 32755.0000 15.7232 20.8283 + 32755.0000 16.3105 21.0569 + 32755.0000 16.8977 21.2855 + 32755.0000 17.4850 21.5142 + 32755.0000 18.0838 21.7105 + 32755.0000 18.6829 21.9060 + 32755.0000 19.2820 22.1015 + 32755.0000 19.8000 22.4500 + 999.9990 999.9990 0.0000 + 39 15 0 37 32770.0000 PontA2 + 32770.0000 0.0000 25.0000 + 32770.0000 0.6592 25.0000 + 32770.0000 1.3184 25.0000 + 32770.0000 1.9000 24.9224 + 32770.0000 1.9000 24.2632 + 32770.0000 1.9000 23.6040 + 32770.0000 1.9000 22.9448 + 32770.0000 1.9000 22.2856 + 32770.0000 1.9000 21.6264 + 32770.0000 2.0859 21.0310 + 32770.0000 2.4906 20.5107 + 32770.0000 2.8953 19.9903 + 32770.0000 3.3000 19.4700 rg + 32770.0000 3.9217 18.9361 + 32770.0000 4.5434 18.4021 + 32770.0000 5.2176 18.0100 + 32770.0000 6.0371 18.0100 + 32770.0000 6.8567 18.0100 + 32770.0000 7.6762 18.0100 + 32770.0000 8.4957 18.0100 + 32770.0000 9.3152 18.0100 + 32770.0000 10.1347 18.0100 + 32770.0000 10.8255 18.3268 + 32770.0000 11.4127 18.8984 + 32770.0000 12.0000 19.4700 rd + 32770.0000 12.9730 19.7330 + 32770.0000 13.9461 19.9960 + 32770.0000 14.9191 20.2590 + 32770.0000 15.8922 20.5219 + 32770.0000 16.8652 20.7849 + 32770.0000 17.8383 21.0479 + 32770.0000 18.8113 21.3109 + 32770.0000 19.4000 21.8682 + 32770.0000 19.4000 22.8761 + 32770.0000 19.4000 23.8841 + 32770.0000 19.4000 24.8920 + 32770.0000 20.3000 25.0000 + 999.9990 999.9990 0.0000 + 40 16 0 37 32800.0000 PontA2 + 32800.0000 0.0000 25.0000 + 32800.0000 0.6592 25.0000 + 32800.0000 1.3184 25.0000 + 32800.0000 1.9000 24.9224 + 32800.0000 1.9000 24.2632 + 32800.0000 1.9000 23.6040 + 32800.0000 1.9000 22.9448 + 32800.0000 1.9000 22.2856 + 32800.0000 1.9000 21.6264 + 32800.0000 2.0859 21.0310 + 32800.0000 2.4906 20.5107 + 32800.0000 2.8953 19.9903 + 32800.0000 3.3000 19.4700 rg + 32800.0000 3.9217 18.9361 + 32800.0000 4.5434 18.4021 + 32800.0000 5.2176 18.0100 + 32800.0000 6.0371 18.0100 + 32800.0000 6.8567 18.0100 + 32800.0000 7.6762 18.0100 + 32800.0000 8.4957 18.0100 + 32800.0000 9.3152 18.0100 + 32800.0000 10.1347 18.0100 + 32800.0000 10.8255 18.3268 + 32800.0000 11.4127 18.8984 + 32800.0000 12.0000 19.4700 rd + 32800.0000 12.9730 19.7330 + 32800.0000 13.9461 19.9960 + 32800.0000 14.9191 20.2590 + 32800.0000 15.8922 20.5219 + 32800.0000 16.8652 20.7849 + 32800.0000 17.8383 21.0479 + 32800.0000 18.8113 21.3109 + 32800.0000 19.4000 21.8682 + 32800.0000 19.4000 22.8761 + 32800.0000 19.4000 23.8841 + 32800.0000 19.4000 24.8920 + 32800.0000 20.3000 25.0000 + 999.9990 999.9990 0.0000 + 41 17 0 37 32820.0000 + 32820.0000 0.0000 22.4500 + 32820.0000 0.2456 22.1553 + 32820.0000 0.5695 21.9613 + 32820.0000 0.9156 21.7959 + 32820.0000 1.2618 21.6305 + 32820.0000 1.6079 21.4651 + 32820.0000 1.9541 21.2997 + 32820.0000 2.2348 21.0537 + 32820.0000 2.4678 20.7490 + 32820.0000 2.7009 20.4442 + 32820.0000 2.9339 20.1395 + 32820.0000 3.1670 19.8347 + 32820.0000 3.4000 19.5300 rg + 32820.0000 3.7416 18.6875 + 32820.0000 4.2211 18.0412 + 32820.0000 5.1296 18.0100 + 32820.0000 6.0387 18.0100 + 32820.0000 6.9478 18.0100 + 32820.0000 7.8569 18.0100 + 32820.0000 8.7660 18.0100 + 32820.0000 9.6751 18.0100 + 32820.0000 10.5373 18.2181 + 32820.0000 11.3582 18.6087 + 32820.0000 12.1791 18.9994 + 32820.0000 13.0000 19.3900 rd + 32820.0000 13.5542 19.6899 + 32820.0000 14.1085 19.9899 + 32820.0000 14.6627 20.2898 + 32820.0000 15.1359 20.5997 + 32820.0000 15.7232 20.8283 + 32820.0000 16.3105 21.0569 + 32820.0000 16.8977 21.2855 + 32820.0000 17.4850 21.5142 + 32820.0000 18.0838 21.7105 + 32820.0000 18.6829 21.9060 + 32820.0000 19.2820 22.1015 + 32820.0000 19.8000 22.4500 + 999.9990 999.9990 0.0000 + 42 17 1 37 32867.5000 + 32867.5000 0.0000 22.5000 + 32867.5000 0.2395 22.1775 + 32867.5000 0.5374 21.9261 + 32867.5000 0.8940 21.7403 + 32867.5000 1.2506 21.5546 + 32867.5000 1.6072 21.3688 + 32867.5000 1.9747 21.2071 + 32867.5000 2.3133 21.0133 + 32867.5000 2.5716 20.7061 + 32867.5000 2.8287 20.3971 + 32867.5000 3.0858 20.0880 + 32867.5000 3.3429 19.7790 + 32867.5000 3.6000 19.4700 rg + 32867.5000 4.0917 18.8134 + 32867.5000 4.6607 18.2650 + 32867.5000 5.4442 18.0241 + 32867.5000 6.2933 18.0100 + 32867.5000 7.1468 18.0100 + 32867.5000 8.0003 18.0100 + 32867.5000 8.8538 18.0100 + 32867.5000 9.7073 18.0100 + 32867.5000 10.5373 18.1140 + 32867.5000 11.3006 18.4248 + 32867.5000 12.0003 18.8949 + 32867.5000 12.7000 19.3650 rd + 32867.5000 13.2935 19.6348 + 32867.5000 13.8871 19.9045 + 32867.5000 14.4806 20.1743 + 32867.5000 15.0337 20.4490 + 32867.5000 15.6437 20.6831 + 32867.5000 16.2538 20.9172 + 32867.5000 16.8638 21.1513 + 32867.5000 17.4739 21.3854 + 32867.5000 18.0897 21.6033 + 32867.5000 18.7057 21.8209 + 32867.5000 19.3216 22.0384 + 32867.5000 19.7500 22.5000 + 999.9990 999.9990 0.0000 + 43 18 0 37 32915.0000 + 32915.0000 0.0000 22.5500 + 32915.0000 0.2335 22.1997 + 32915.0000 0.5053 21.8909 + 32915.0000 0.8724 21.6847 + 32915.0000 1.2394 21.4786 + 32915.0000 1.6065 21.2725 + 32915.0000 1.9954 21.1145 + 32915.0000 2.3918 20.9729 + 32915.0000 2.6754 20.6632 + 32915.0000 2.9565 20.3499 + 32915.0000 3.2377 20.0366 + 32915.0000 3.5188 19.7233 + 32915.0000 3.8000 19.4100 rg + 32915.0000 4.4418 18.9393 + 32915.0000 5.1003 18.4888 + 32915.0000 5.7588 18.0382 + 32915.0000 6.5479 18.0100 + 32915.0000 7.3458 18.0100 + 32915.0000 8.1437 18.0100 + 32915.0000 8.9416 18.0100 + 32915.0000 9.7395 18.0100 + 32915.0000 10.5374 18.0100 + 32915.0000 11.2431 18.2409 + 32915.0000 11.8215 18.7905 + 32915.0000 12.4000 19.3400 rd + 32915.0000 13.0328 19.5796 + 32915.0000 13.6657 19.8192 + 32915.0000 14.2985 20.0587 + 32915.0000 14.9314 20.2983 + 32915.0000 15.5642 20.5379 + 32915.0000 16.1971 20.7775 + 32915.0000 16.8299 21.0170 + 32915.0000 17.4628 21.2566 + 32915.0000 18.0956 21.4962 + 32915.0000 18.7285 21.7358 + 32915.0000 19.3613 21.9753 + 32915.0000 19.7000 22.5500 + 999.9990 999.9990 0.0000 + 44 18 1 37 32960.0000 + 32960.0000 0.0800 22.3940 + 32960.0000 0.3314 22.0703 + 32960.0000 0.6134 21.7797 + 32960.0000 0.9716 21.5713 + 32960.0000 1.3298 21.3630 + 32960.0000 1.6915 21.1618 + 32960.0000 2.0770 21.0121 + 32960.0000 2.4651 20.8698 + 32960.0000 2.7496 20.5697 + 32960.0000 3.0322 20.2668 + 32960.0000 3.3148 19.9639 + 32960.0000 3.5974 19.6609 + 32960.0000 3.8800 19.3580 rg + 32960.0000 4.5246 18.9031 + 32960.0000 5.1863 18.4694 + 32960.0000 5.8480 18.0358 + 32960.0000 6.6273 17.9920 + 32960.0000 7.4191 17.9920 + 32960.0000 8.2109 17.9920 + 32960.0000 9.0026 17.9920 + 32960.0000 9.7944 17.9920 + 32960.0000 10.5861 17.9920 + 32960.0000 11.2907 18.2107 + 32960.0000 11.8653 18.7554 + 32960.0000 12.4400 19.3000 rd + 32960.0000 13.0579 19.5400 + 32960.0000 13.6757 19.7800 + 32960.0000 14.2936 20.0201 + 32960.0000 14.9115 20.2601 + 32960.0000 15.5294 20.5001 + 32960.0000 16.1472 20.7401 + 32960.0000 16.7651 20.9801 + 32960.0000 17.3830 21.2202 + 32960.0000 18.0009 21.4602 + 32960.0000 18.6187 21.7002 + 32960.0000 19.2366 21.9402 + 32960.0000 19.6000 22.4760 + 999.9990 999.9990 0.0000 + 45 18 2 37 33005.0000 + 33005.0000 0.1600 22.2380 + 33005.0000 0.4293 21.9409 + 33005.0000 0.7215 21.6686 + 33005.0000 1.0709 21.4579 + 33005.0000 1.4203 21.2473 + 33005.0000 1.7766 21.0511 + 33005.0000 2.1586 20.9098 + 33005.0000 2.5383 20.7667 + 33005.0000 2.8238 20.4762 + 33005.0000 3.1079 20.1837 + 33005.0000 3.3919 19.8911 + 33005.0000 3.6760 19.5986 + 33005.0000 3.9600 19.3060 rg + 33005.0000 4.6075 18.8668 + 33005.0000 5.2723 18.4501 + 33005.0000 5.9371 18.0333 + 33005.0000 6.7067 17.9740 + 33005.0000 7.4924 17.9740 + 33005.0000 8.2780 17.9740 + 33005.0000 9.0636 17.9740 + 33005.0000 9.8492 17.9740 + 33005.0000 10.6349 17.9740 + 33005.0000 11.3383 18.1805 + 33005.0000 11.9091 18.7203 + 33005.0000 12.4800 19.2600 rd + 33005.0000 13.0829 19.5005 + 33005.0000 13.6858 19.7409 + 33005.0000 14.2887 19.9814 + 33005.0000 14.8916 20.2219 + 33005.0000 15.4945 20.4623 + 33005.0000 16.0974 20.7028 + 33005.0000 16.7003 20.9432 + 33005.0000 17.3032 21.1837 + 33005.0000 17.9061 21.4242 + 33005.0000 18.5090 21.6646 + 33005.0000 19.1119 21.9051 + 33005.0000 19.5000 22.4020 + 999.9990 999.9990 0.0000 + 46 18 3 37 33050.0000 + 33050.0000 0.2400 22.0820 + 33050.0000 0.5271 21.8114 + 33050.0000 0.8296 21.5574 + 33050.0000 1.1702 21.3445 + 33050.0000 1.5107 21.1317 + 33050.0000 1.8616 20.9403 + 33050.0000 2.2401 20.8074 + 33050.0000 2.6116 20.6636 + 33050.0000 2.8980 20.3828 + 33050.0000 3.1835 20.1006 + 33050.0000 3.4690 19.8184 + 33050.0000 3.7545 19.5362 + 33050.0000 4.0400 19.2540 rg + 33050.0000 4.6903 18.8306 + 33050.0000 5.3583 18.4308 + 33050.0000 6.0263 18.0309 + 33050.0000 6.7861 17.9560 + 33050.0000 7.5656 17.9560 + 33050.0000 8.3451 17.9560 + 33050.0000 9.1246 17.9560 + 33050.0000 9.9041 17.9560 + 33050.0000 10.6836 17.9560 + 33050.0000 11.3859 18.1503 + 33050.0000 11.9529 18.6852 + 33050.0000 12.5200 19.2200 rd + 33050.0000 13.1079 19.4609 + 33050.0000 13.6959 19.7018 + 33050.0000 14.2838 19.9427 + 33050.0000 14.8717 20.1836 + 33050.0000 15.4596 20.4245 + 33050.0000 16.0476 20.6654 + 33050.0000 16.6355 20.9063 + 33050.0000 17.2234 21.1473 + 33050.0000 17.8114 21.3882 + 33050.0000 18.3993 21.6291 + 33050.0000 18.9872 21.8700 + 33050.0000 19.4000 22.3280 + 999.9990 999.9990 0.0000 + 47 18 4 37 33095.0000 + 33095.0000 0.3200 21.9260 + 33095.0000 0.6250 21.6820 + 33095.0000 0.9377 21.4463 + 33095.0000 1.2694 21.2312 + 33095.0000 1.6011 21.0160 + 33095.0000 1.9467 20.8296 + 33095.0000 2.3217 20.7051 + 33095.0000 2.6848 20.5605 + 33095.0000 2.9722 20.2893 + 33095.0000 3.2592 20.0175 + 33095.0000 3.5461 19.7457 + 33095.0000 3.8331 19.4738 + 33095.0000 4.1200 19.2020 rg + 33095.0000 4.7732 18.7944 + 33095.0000 5.4443 18.4114 + 33095.0000 6.1155 18.0285 + 33095.0000 6.8655 17.9380 + 33095.0000 7.6389 17.9380 + 33095.0000 8.4123 17.9380 + 33095.0000 9.1856 17.9380 + 33095.0000 9.9590 17.9380 + 33095.0000 10.7324 17.9380 + 33095.0000 11.4335 18.1201 + 33095.0000 11.9967 18.6501 + 33095.0000 12.5600 19.1800 rd + 33095.0000 13.1330 19.4214 + 33095.0000 13.7059 19.6627 + 33095.0000 14.2789 19.9041 + 33095.0000 14.8518 20.1454 + 33095.0000 15.4248 20.3868 + 33095.0000 15.9977 20.6281 + 33095.0000 16.5707 20.8695 + 33095.0000 17.1437 21.1108 + 33095.0000 17.7166 21.3522 + 33095.0000 18.2896 21.5935 + 33095.0000 18.8625 21.8349 + 33095.0000 19.3000 22.2540 + 999.9990 999.9990 0.0000 + 48 19 0 37 33140.0000 + 33140.0000 0.4000 21.7700 + 33140.0000 0.7229 21.5526 + 33140.0000 1.0458 21.3352 + 33140.0000 1.3687 21.1178 + 33140.0000 1.6916 20.9003 + 33140.0000 2.0317 20.7188 + 33140.0000 2.4033 20.6027 + 33140.0000 2.7581 20.4573 + 33140.0000 3.0465 20.1959 + 33140.0000 3.3348 19.9344 + 33140.0000 3.6232 19.6729 + 33140.0000 3.9116 19.4115 + 33140.0000 4.2000 19.1500 rg + 33140.0000 4.8560 18.7581 + 33140.0000 5.5303 18.3921 + 33140.0000 6.2046 18.0261 + 33140.0000 6.9449 17.9200 + 33140.0000 7.7122 17.9200 + 33140.0000 8.4794 17.9200 + 33140.0000 9.2466 17.9200 + 33140.0000 10.0139 17.9200 + 33140.0000 10.7811 17.9200 + 33140.0000 11.4811 18.0899 + 33140.0000 12.0405 18.6150 + 33140.0000 12.6000 19.1400 rd + 33140.0000 13.1580 19.3818 + 33140.0000 13.7160 19.6236 + 33140.0000 14.2740 19.8654 + 33140.0000 14.8319 20.1072 + 33140.0000 15.3899 20.3490 + 33140.0000 15.9479 20.5908 + 33140.0000 16.5059 20.8326 + 33140.0000 17.0639 21.0744 + 33140.0000 17.6219 21.3161 + 33140.0000 18.1799 21.5579 + 33140.0000 18.7378 21.7997 + 33140.0000 19.2000 22.1800 + 999.9990 999.9990 0.0000 + 49 19 1 37 33187.5000 + 33187.5000 0.3333 21.7550 + 33187.5000 0.6776 21.5513 + 33187.5000 1.0094 21.3237 + 33187.5000 1.3396 21.0932 + 33187.5000 1.6699 20.8627 + 33187.5000 2.0145 20.6622 + 33187.5000 2.3890 20.5235 + 33187.5000 2.7604 20.3816 + 33187.5000 3.0720 20.1325 + 33187.5000 3.3790 19.8732 + 33187.5000 3.6860 19.6138 + 33187.5000 3.9930 19.3544 + 33187.5000 4.3000 19.0950 rg + 33187.5000 4.9556 18.7187 + 33187.5000 5.6286 18.3678 + 33187.5000 6.3016 18.0169 + 33187.5000 7.0330 17.9000 + 33187.5000 7.7926 17.9000 + 33187.5000 8.5521 17.9000 + 33187.5000 9.3116 17.9000 + 33187.5000 10.0711 17.9000 + 33187.5000 10.8307 17.9000 + 33187.5000 11.5294 18.0532 + 33187.5000 12.0814 18.5750 + 33187.5000 12.6333 19.0967 rd + 33187.5000 13.1733 19.3371 + 33187.5000 13.7133 19.5774 + 33187.5000 14.2533 19.8178 + 33187.5000 14.7933 20.0582 + 33187.5000 15.3333 20.2986 + 33187.5000 15.8733 20.5390 + 33187.5000 16.4132 20.7794 + 33187.5000 16.9532 21.0197 + 33187.5000 17.4932 21.2601 + 33187.5000 18.0332 21.5005 + 33187.5000 18.5732 21.7409 + 33187.5000 19.0333 22.0967 + 999.9990 999.9990 0.0000 + 50 19 2 37 33235.0000 + 33235.0000 0.2667 21.7400 + 33235.0000 0.6324 21.5499 + 33235.0000 0.9730 21.3122 + 33235.0000 1.3106 21.0686 + 33235.0000 1.6481 20.8251 + 33235.0000 1.9972 20.6056 + 33235.0000 2.3748 20.4444 + 33235.0000 2.7628 20.3058 + 33235.0000 3.0975 20.0692 + 33235.0000 3.4231 19.8119 + 33235.0000 3.7487 19.5546 + 33235.0000 4.0744 19.2973 + 33235.0000 4.4000 19.0400 rg + 33235.0000 5.0552 18.6792 + 33235.0000 5.7269 18.3434 + 33235.0000 6.3985 18.0077 + 33235.0000 7.1211 17.8800 + 33235.0000 7.8729 17.8800 + 33235.0000 8.6248 17.8800 + 33235.0000 9.3766 17.8800 + 33235.0000 10.1284 17.8800 + 33235.0000 10.8802 17.8800 + 33235.0000 11.5777 18.0165 + 33235.0000 12.1222 18.5349 + 33235.0000 12.6667 19.0533 rd + 33235.0000 13.1887 19.2923 + 33235.0000 13.7106 19.5313 + 33235.0000 14.2326 19.7703 + 33235.0000 14.7546 20.0092 + 33235.0000 15.2766 20.2482 + 33235.0000 15.7986 20.4872 + 33235.0000 16.3206 20.7261 + 33235.0000 16.8426 20.9651 + 33235.0000 17.3646 21.2041 + 33235.0000 17.8866 21.4431 + 33235.0000 18.4086 21.6820 + 33235.0000 18.8667 22.0133 + 999.9990 999.9990 0.0000 + 51 19 3 37 33282.5000 + 33282.5000 0.2000 21.7250 + 33282.5000 0.5871 21.5486 + 33282.5000 0.9366 21.3007 + 33282.5000 1.2815 21.0441 + 33282.5000 1.6264 20.7875 + 33282.5000 1.9800 20.5489 + 33282.5000 2.3605 20.3652 + 33282.5000 2.7651 20.2300 + 33282.5000 3.1230 20.0059 + 33282.5000 3.4672 19.7506 + 33282.5000 3.8115 19.4954 + 33282.5000 4.1557 19.2402 + 33282.5000 4.5000 18.9850 rg + 33282.5000 5.1548 18.6397 + 33282.5000 5.8252 18.3191 + 33282.5000 6.4955 17.9985 + 33282.5000 7.2092 17.8600 + 33282.5000 7.9533 17.8600 + 33282.5000 8.6975 17.8600 + 33282.5000 9.4416 17.8600 + 33282.5000 10.1857 17.8600 + 33282.5000 10.9298 17.8600 + 33282.5000 11.6261 17.9798 + 33282.5000 12.1630 18.4949 + 33282.5000 12.7000 19.0100 rd + 33282.5000 13.2040 19.2476 + 33282.5000 13.7080 19.4851 + 33282.5000 14.2120 19.7227 + 33282.5000 14.7160 19.9603 + 33282.5000 15.2200 20.1978 + 33282.5000 15.7240 20.4354 + 33282.5000 16.2279 20.6729 + 33282.5000 16.7319 20.9105 + 33282.5000 17.2359 21.1481 + 33282.5000 17.7399 21.3856 + 33282.5000 18.2439 21.6232 + 33282.5000 18.7000 21.9300 + 999.9990 999.9990 0.0000 + 52 19 4 37 33330.0000 + 33330.0000 0.1333 21.7100 + 33330.0000 0.5419 21.5473 + 33330.0000 0.9002 21.2892 + 33330.0000 1.2525 21.0195 + 33330.0000 1.6047 20.7499 + 33330.0000 1.9627 20.4923 + 33330.0000 2.3463 20.2860 + 33330.0000 2.7675 20.1542 + 33330.0000 3.1485 19.9425 + 33330.0000 3.5114 19.6894 + 33330.0000 3.8742 19.4363 + 33330.0000 4.2371 19.1831 + 33330.0000 4.6000 18.9300 rg + 33330.0000 5.2544 18.6003 + 33330.0000 5.9234 18.2948 + 33330.0000 6.5925 17.9892 + 33330.0000 7.2973 17.8400 + 33330.0000 8.0337 17.8400 + 33330.0000 8.7701 17.8400 + 33330.0000 9.5066 17.8400 + 33330.0000 10.2430 17.8400 + 33330.0000 10.9794 17.8400 + 33330.0000 11.6744 17.9431 + 33330.0000 12.2039 18.4549 + 33330.0000 12.7333 18.9667 rd + 33330.0000 13.2193 19.2028 + 33330.0000 13.7053 19.4390 + 33330.0000 14.1913 19.6751 + 33330.0000 14.6773 19.9113 + 33330.0000 15.1633 20.1474 + 33330.0000 15.6493 20.3836 + 33330.0000 16.1353 20.6197 + 33330.0000 16.6213 20.8559 + 33330.0000 17.1073 21.0920 + 33330.0000 17.5933 21.3282 + 33330.0000 18.0793 21.5644 + 33330.0000 18.5333 21.8467 + 999.9990 999.9990 0.0000 + 53 19 5 37 33377.5000 + 33377.5000 0.0667 21.6950 + 33377.5000 0.4966 21.5459 + 33377.5000 0.8638 21.2777 + 33377.5000 1.2234 20.9950 + 33377.5000 1.5830 20.7123 + 33377.5000 1.9455 20.4357 + 33377.5000 2.3321 20.2068 + 33377.5000 2.7698 20.0784 + 33377.5000 3.1740 19.8792 + 33377.5000 3.5555 19.6281 + 33377.5000 3.9370 19.3771 + 33377.5000 4.3185 19.1260 + 33377.5000 4.7000 18.8750 rg + 33377.5000 5.3540 18.5608 + 33377.5000 6.0217 18.2704 + 33377.5000 6.6894 17.9800 + 33377.5000 7.3854 17.8200 + 33377.5000 8.1141 17.8200 + 33377.5000 8.8428 17.8200 + 33377.5000 9.5715 17.8200 + 33377.5000 10.3002 17.8200 + 33377.5000 11.0290 17.8200 + 33377.5000 11.7227 17.9064 + 33377.5000 12.2447 18.4149 + 33377.5000 12.7667 18.9233 rd + 33377.5000 13.2347 19.1581 + 33377.5000 13.7027 19.3928 + 33377.5000 14.1707 19.6276 + 33377.5000 14.6387 19.8623 + 33377.5000 15.1067 20.0971 + 33377.5000 15.5747 20.3318 + 33377.5000 16.0426 20.5665 + 33377.5000 16.5106 20.8013 + 33377.5000 16.9786 21.0360 + 33377.5000 17.4466 21.2708 + 33377.5000 17.9146 21.5055 + 33377.5000 18.3667 21.7633 + 999.9990 999.9990 0.0000 + 54 20 0 37 33425.0000 + 33425.0000 0.0000 21.6800 + 33425.0000 0.4514 21.5446 + 33425.0000 0.8274 21.2661 + 33425.0000 1.1943 20.9704 + 33425.0000 1.5613 20.6747 + 33425.0000 1.9282 20.3790 + 33425.0000 2.3178 20.1276 + 33425.0000 2.7722 20.0026 + 33425.0000 3.1995 19.8159 + 33425.0000 3.5996 19.5669 + 33425.0000 3.9998 19.3179 + 33425.0000 4.3999 19.0690 + 33425.0000 4.8000 18.8200 rg + 33425.0000 5.4536 18.5213 + 33425.0000 6.1200 18.2461 + 33425.0000 6.7864 17.9708 + 33425.0000 7.4735 17.8000 + 33425.0000 8.1945 17.8000 + 33425.0000 8.9155 17.8000 + 33425.0000 9.6365 17.8000 + 33425.0000 10.3575 17.8000 + 33425.0000 11.0785 17.8000 + 33425.0000 11.7710 17.8697 + 33425.0000 12.2855 18.3749 + 33425.0000 12.8000 18.8800 rd + 33425.0000 13.2500 19.1133 + 33425.0000 13.7000 19.3467 + 33425.0000 14.1500 19.5800 + 33425.0000 14.6000 19.8133 + 33425.0000 15.0500 20.0467 + 33425.0000 15.5000 20.2800 + 33425.0000 15.9500 20.5133 + 33425.0000 16.4000 20.7467 + 33425.0000 16.8500 20.9800 + 33425.0000 17.3000 21.2133 + 33425.0000 17.7500 21.4467 + 33425.0000 18.2000 21.6800 + 999.9990 999.9990 0.0000 + 55 20 1 37 33477.0000 + 33477.0000 0.0000 21.6500 + 33477.0000 0.4550 21.5022 + 33477.0000 0.8329 21.2134 + 33477.0000 1.2024 20.9089 + 33477.0000 1.5718 20.6044 + 33477.0000 1.9413 20.2999 + 33477.0000 2.3289 20.0309 + 33477.0000 2.7902 19.9056 + 33477.0000 3.2306 19.7315 + 33477.0000 3.6430 19.4891 + 33477.0000 4.0553 19.2468 + 33477.0000 4.4677 19.0044 + 33477.0000 4.8800 18.7620 rg + 33477.0000 5.5341 18.4830 + 33477.0000 6.1984 18.2224 + 33477.0000 6.8626 17.9618 + 33477.0000 7.5435 17.7847 + 33477.0000 8.2568 17.7800 + 33477.0000 8.9708 17.7800 + 33477.0000 9.6848 17.7800 + 33477.0000 10.3989 17.7800 + 33477.0000 11.1129 17.7800 + 33477.0000 11.8041 17.8358 + 33477.0000 12.3140 18.3303 + 33477.0000 12.8200 18.8340 rd + 33477.0000 13.2583 19.0687 + 33477.0000 13.6967 19.3033 + 33477.0000 14.1350 19.5380 + 33477.0000 14.5733 19.7727 + 33477.0000 15.0117 20.0073 + 33477.0000 15.4500 20.2420 + 33477.0000 15.8883 20.4767 + 33477.0000 16.3267 20.7113 + 33477.0000 16.7650 20.9460 + 33477.0000 17.2033 21.1807 + 33477.0000 17.6417 21.4153 + 33477.0000 18.0800 21.6500 + 999.9990 999.9990 0.0000 + 56 20 2 37 33529.0000 + 33529.0000 0.0000 21.6200 + 33529.0000 0.4587 21.4599 + 33529.0000 0.8384 21.1606 + 33529.0000 1.2104 20.8474 + 33529.0000 1.5824 20.5341 + 33529.0000 1.9544 20.2208 + 33529.0000 2.3400 19.9341 + 33529.0000 2.8082 19.8085 + 33529.0000 3.2617 19.6472 + 33529.0000 3.6863 19.4114 + 33529.0000 4.1109 19.1756 + 33529.0000 4.5354 18.9398 + 33529.0000 4.9600 18.7040 rg + 33529.0000 5.6146 18.4446 + 33529.0000 6.2767 18.1987 + 33529.0000 6.9389 17.9527 + 33529.0000 7.6134 17.7694 + 33529.0000 8.3190 17.7600 + 33529.0000 9.0261 17.7600 + 33529.0000 9.7331 17.7600 + 33529.0000 10.4402 17.7600 + 33529.0000 11.1472 17.7600 + 33529.0000 11.8372 17.8018 + 33529.0000 12.3426 18.2857 + 33529.0000 12.8400 18.7880 rd + 33529.0000 13.2667 19.0240 + 33529.0000 13.6933 19.2600 + 33529.0000 14.1200 19.4960 + 33529.0000 14.5467 19.7320 + 33529.0000 14.9733 19.9680 + 33529.0000 15.4000 20.2040 + 33529.0000 15.8267 20.4400 + 33529.0000 16.2533 20.6760 + 33529.0000 16.6800 20.9120 + 33529.0000 17.1067 21.1480 + 33529.0000 17.5333 21.3840 + 33529.0000 17.9600 21.6200 + 999.9990 999.9990 0.0000 + 57 20 3 37 33581.0000 + 33581.0000 0.0000 21.5900 + 33581.0000 0.4623 21.4175 + 33581.0000 0.8440 21.1079 + 33581.0000 1.2185 20.7858 + 33581.0000 1.5930 20.4638 + 33581.0000 1.9675 20.1417 + 33581.0000 2.3511 19.8374 + 33581.0000 2.8262 19.7114 + 33581.0000 3.2929 19.5629 + 33581.0000 3.7296 19.3336 + 33581.0000 4.1664 19.1044 + 33581.0000 4.6032 18.8752 + 33581.0000 5.0400 18.6460 rg + 33581.0000 5.6951 18.4062 + 33581.0000 6.3551 18.1750 + 33581.0000 7.0151 17.9437 + 33581.0000 7.6834 17.7542 + 33581.0000 8.3813 17.7400 + 33581.0000 9.0814 17.7400 + 33581.0000 9.7815 17.7400 + 33581.0000 10.4815 17.7400 + 33581.0000 11.1816 17.7400 + 33581.0000 11.8703 17.7679 + 33581.0000 12.3711 18.2411 + 33581.0000 12.8600 18.7420 rd + 33581.0000 13.2750 18.9793 + 33581.0000 13.6900 19.2167 + 33581.0000 14.1050 19.4540 + 33581.0000 14.5200 19.6913 + 33581.0000 14.9350 19.9287 + 33581.0000 15.3500 20.1660 + 33581.0000 15.7650 20.4033 + 33581.0000 16.1800 20.6407 + 33581.0000 16.5950 20.8780 + 33581.0000 17.0100 21.1153 + 33581.0000 17.4250 21.3527 + 33581.0000 17.8400 21.5900 + 999.9990 999.9990 0.0000 + 58 20 4 37 33633.0000 + 33633.0000 0.0000 21.5600 + 33633.0000 0.4659 21.3751 + 33633.0000 0.8495 21.0551 + 33633.0000 1.2265 20.7243 + 33633.0000 1.6036 20.3935 + 33633.0000 1.9806 20.0626 + 33633.0000 2.3622 19.7407 + 33633.0000 2.8442 19.6144 + 33633.0000 3.3240 19.4785 + 33633.0000 3.7730 19.2559 + 33633.0000 4.2220 19.0333 + 33633.0000 4.6710 18.8106 + 33633.0000 5.1200 18.5880 rg + 33633.0000 5.7756 18.3679 + 33633.0000 6.4335 18.1513 + 33633.0000 7.0913 17.9346 + 33633.0000 7.7533 17.7389 + 33633.0000 8.4436 17.7200 + 33633.0000 9.1367 17.7200 + 33633.0000 9.8298 17.7200 + 33633.0000 10.5229 17.7200 + 33633.0000 11.2159 17.7200 + 33633.0000 11.9033 17.7339 + 33633.0000 12.3996 18.1965 + 33633.0000 12.8800 18.6960 rd + 33633.0000 13.2833 18.9347 + 33633.0000 13.6867 19.1733 + 33633.0000 14.0900 19.4120 + 33633.0000 14.4933 19.6507 + 33633.0000 14.8967 19.8893 + 33633.0000 15.3000 20.1280 + 33633.0000 15.7033 20.3667 + 33633.0000 16.1067 20.6053 + 33633.0000 16.5100 20.8440 + 33633.0000 16.9133 21.0827 + 33633.0000 17.3167 21.3213 + 33633.0000 17.7200 21.5600 + 999.9990 999.9990 0.0000 + 59 21 0 37 33685.0000 + 33685.0000 0.0000 21.5300 + 33685.0000 0.4696 21.3328 + 33685.0000 0.8550 21.0024 + 33685.0000 1.2346 20.6628 + 33685.0000 1.6141 20.3231 + 33685.0000 1.9937 19.9835 + 33685.0000 2.3733 19.6439 + 33685.0000 2.8622 19.5173 + 33685.0000 3.3551 19.3942 + 33685.0000 3.8163 19.1782 + 33685.0000 4.2775 18.9621 + 33685.0000 4.7388 18.7461 + 33685.0000 5.2000 18.5300 rg + 33685.0000 5.8562 18.3295 + 33685.0000 6.5119 18.1275 + 33685.0000 7.1676 17.9256 + 33685.0000 7.8233 17.7236 + 33685.0000 8.5059 17.7000 + 33685.0000 9.1920 17.7000 + 33685.0000 9.8781 17.7000 + 33685.0000 10.5642 17.7000 + 33685.0000 11.2503 17.7000 + 33685.0000 11.9364 17.7000 + 33685.0000 12.4281 18.1519 + 33685.0000 12.9000 18.6500 rd + 33685.0000 13.2917 18.8900 + 33685.0000 13.6833 19.1300 + 33685.0000 14.0750 19.3700 + 33685.0000 14.4667 19.6100 + 33685.0000 14.8583 19.8500 + 33685.0000 15.2500 20.0900 + 33685.0000 15.6417 20.3300 + 33685.0000 16.0333 20.5700 + 33685.0000 16.4250 20.8100 + 33685.0000 16.8167 21.0500 + 33685.0000 17.2083 21.2900 + 33685.0000 17.6000 21.5300 + 999.9990 999.9990 0.0000 + 60 21 1 37 33732.0000 + 33732.0000 0.1200 21.4200 + 33732.0000 0.5685 21.1949 + 33732.0000 0.9496 20.8632 + 33732.0000 1.3260 20.5242 + 33732.0000 1.7025 20.1852 + 33732.0000 2.0789 19.8462 + 33732.0000 2.4678 19.5318 + 33732.0000 2.9564 19.4120 + 33732.0000 3.4467 19.2896 + 33732.0000 3.9100 19.0862 + 33732.0000 4.3734 18.8828 + 33732.0000 4.8367 18.6794 + 33732.0000 5.3000 18.4760 rg + 33732.0000 5.9513 18.2832 + 33732.0000 6.6031 18.0924 + 33732.0000 7.2550 17.9016 + 33732.0000 7.9069 17.7108 + 33732.0000 8.5823 17.6800 + 33732.0000 9.2618 17.6800 + 33732.0000 9.9413 17.6800 + 33732.0000 10.6208 17.6800 + 33732.0000 11.3003 17.6800 + 33732.0000 11.9799 17.6800 + 33732.0000 12.4724 18.1149 + 33732.0000 12.9400 18.6080 rd + 33732.0000 13.3183 18.8448 + 33732.0000 13.6967 19.0817 + 33732.0000 14.0750 19.3185 + 33732.0000 14.4533 19.5553 + 33732.0000 14.8317 19.7922 + 33732.0000 15.2100 20.0290 + 33732.0000 15.5883 20.2658 + 33732.0000 15.9667 20.5027 + 33732.0000 16.3450 20.7395 + 33732.0000 16.7233 20.9763 + 33732.0000 17.1017 21.2132 + 33732.0000 17.4800 21.4500 + 999.9990 999.9990 0.0000 + 61 21 2 37 33779.0000 + 33779.0000 0.2400 21.3100 + 33779.0000 0.6673 21.0570 + 33779.0000 1.0442 20.7241 + 33779.0000 1.4175 20.3857 + 33779.0000 1.7908 20.0472 + 33779.0000 2.1642 19.7088 + 33779.0000 2.5624 19.4197 + 33779.0000 3.0505 19.3067 + 33779.0000 3.5383 19.1849 + 33779.0000 4.0038 18.9942 + 33779.0000 4.4692 18.8035 + 33779.0000 4.9346 18.6127 + 33779.0000 5.4000 18.4220 rg + 33779.0000 6.0463 18.2369 + 33779.0000 6.6944 18.0572 + 33779.0000 7.3425 17.8776 + 33779.0000 7.9905 17.6979 + 33779.0000 8.6587 17.6600 + 33779.0000 9.3316 17.6600 + 33779.0000 10.0045 17.6600 + 33779.0000 10.6774 17.6600 + 33779.0000 11.3504 17.6600 + 33779.0000 12.0233 17.6600 + 33779.0000 12.5167 18.0780 + 33779.0000 12.9800 18.5660 rd + 33779.0000 13.3450 18.7997 + 33779.0000 13.7100 19.0333 + 33779.0000 14.0750 19.2670 + 33779.0000 14.4400 19.5007 + 33779.0000 14.8050 19.7343 + 33779.0000 15.1700 19.9680 + 33779.0000 15.5350 20.2017 + 33779.0000 15.9000 20.4353 + 33779.0000 16.2650 20.6690 + 33779.0000 16.6300 20.9027 + 33779.0000 16.9950 21.1363 + 33779.0000 17.3600 21.3700 + 999.9990 999.9990 0.0000 + 62 21 3 37 33826.0000 + 33826.0000 0.3600 21.2000 + 33826.0000 0.7662 20.9191 + 33826.0000 1.1388 20.5849 + 33826.0000 1.5090 20.2471 + 33826.0000 1.8792 19.9092 + 33826.0000 2.2494 19.5714 + 33826.0000 2.6569 19.3075 + 33826.0000 3.1447 19.2013 + 33826.0000 3.6300 19.0803 + 33826.0000 4.0975 18.9022 + 33826.0000 4.5650 18.7241 + 33826.0000 5.0325 18.5461 + 33826.0000 5.5000 18.3680 rg + 33826.0000 6.1414 18.1906 + 33826.0000 6.7857 18.0221 + 33826.0000 7.4299 17.8536 + 33826.0000 8.0742 17.6851 + 33826.0000 8.7350 17.6400 + 33826.0000 9.4014 17.6400 + 33826.0000 10.0677 17.6400 + 33826.0000 10.7341 17.6400 + 33826.0000 11.4004 17.6400 + 33826.0000 12.0668 17.6400 + 33826.0000 12.5610 18.0410 + 33826.0000 13.0200 18.5240 rd + 33826.0000 13.3717 18.7545 + 33826.0000 13.7233 18.9850 + 33826.0000 14.0750 19.2155 + 33826.0000 14.4267 19.4460 + 33826.0000 14.7783 19.6765 + 33826.0000 15.1300 19.9070 + 33826.0000 15.4817 20.1375 + 33826.0000 15.8333 20.3680 + 33826.0000 16.1850 20.5985 + 33826.0000 16.5367 20.8290 + 33826.0000 16.8883 21.0595 + 33826.0000 17.2400 21.2900 + 999.9990 999.9990 0.0000 + 63 21 4 37 33873.0000 + 33873.0000 0.4800 21.0900 + 33873.0000 0.8651 20.7812 + 33873.0000 1.2334 20.4458 + 33873.0000 1.6004 20.1085 + 33873.0000 1.9675 19.7713 + 33873.0000 2.3346 19.4340 + 33873.0000 2.7515 19.1954 + 33873.0000 3.2389 19.0960 + 33873.0000 3.7216 18.9756 + 33873.0000 4.1912 18.8102 + 33873.0000 4.6608 18.6448 + 33873.0000 5.1304 18.4794 + 33873.0000 5.6000 18.3140 rg + 33873.0000 6.2365 18.1442 + 33873.0000 6.8770 17.9869 + 33873.0000 7.5174 17.8296 + 33873.0000 8.1578 17.6722 + 33873.0000 8.8114 17.6200 + 33873.0000 9.4712 17.6200 + 33873.0000 10.1310 17.6200 + 33873.0000 10.7907 17.6200 + 33873.0000 11.4505 17.6200 + 33873.0000 12.1102 17.6200 + 33873.0000 12.6053 18.0040 + 33873.0000 13.0600 18.4820 rd + 33873.0000 13.3983 18.7093 + 33873.0000 13.7367 18.9367 + 33873.0000 14.0750 19.1640 + 33873.0000 14.4133 19.3913 + 33873.0000 14.7517 19.6187 + 33873.0000 15.0900 19.8460 + 33873.0000 15.4283 20.0733 + 33873.0000 15.7667 20.3007 + 33873.0000 16.1050 20.5280 + 33873.0000 16.4433 20.7553 + 33873.0000 16.7817 20.9827 + 33873.0000 17.1200 21.2100 + 999.9990 999.9990 0.0000 + 64 22 0 37 33920.0000 + 33920.0000 0.6000 20.9800 + 33920.0000 0.9640 20.6433 + 33920.0000 1.3279 20.3067 + 33920.0000 1.6919 19.9700 + 33920.0000 2.0559 19.6333 + 33920.0000 2.4199 19.2966 + 33920.0000 2.8460 19.0833 + 33920.0000 3.3331 18.9907 + 33920.0000 3.8132 18.8710 + 33920.0000 4.2849 18.7182 + 33920.0000 4.7566 18.5655 + 33920.0000 5.2283 18.4127 + 33920.0000 5.7000 18.2600 rg + 33920.0000 6.3316 18.0979 + 33920.0000 6.9682 17.9517 + 33920.0000 7.6048 17.8056 + 33920.0000 8.2414 17.6594 + 33920.0000 8.8878 17.6000 + 33920.0000 9.5410 17.6000 + 33920.0000 10.1942 17.6000 + 33920.0000 10.8473 17.6000 + 33920.0000 11.5005 17.6000 + 33920.0000 12.1537 17.6000 + 33920.0000 12.6495 17.9670 + 33920.0000 13.1000 18.4400 rd + 33920.0000 13.4250 18.6642 + 33920.0000 13.7500 18.8883 + 33920.0000 14.0750 19.1125 + 33920.0000 14.4000 19.3367 + 33920.0000 14.7250 19.5608 + 33920.0000 15.0500 19.7850 + 33920.0000 15.3750 20.0092 + 33920.0000 15.7000 20.2333 + 33920.0000 16.0250 20.4575 + 33920.0000 16.3500 20.6817 + 33920.0000 16.6750 20.9058 + 33920.0000 17.0000 21.1300 + 999.9990 999.9990 0.0000 + 65 22 1 37 33963.7500 + 33963.7500 0.6000 20.9725 + 33963.7500 0.9655 20.6239 + 33963.7500 1.3311 20.2753 + 33963.7500 1.6966 19.9267 + 33963.7500 2.0622 19.5781 + 33963.7500 2.4277 19.2295 + 33963.7500 2.8528 18.9979 + 33963.7500 3.3498 18.9069 + 33963.7500 3.8411 18.7932 + 33963.7500 4.3245 18.6474 + 33963.7500 4.8080 18.5016 + 33963.7500 5.2915 18.3558 + 33963.7500 5.7750 18.2100 rg + 33963.7500 6.4035 18.0586 + 33963.7500 7.0364 17.9221 + 33963.7500 7.6693 17.7857 + 33963.7500 8.3023 17.6492 + 33963.7500 8.9430 17.5825 + 33963.7500 9.5906 17.5825 + 33963.7500 10.2382 17.5825 + 33963.7500 10.8859 17.5825 + 33963.7500 11.5335 17.5825 + 33963.7500 12.1812 17.5825 + 33963.7500 12.6795 17.9299 + 33963.7500 13.1250 18.4000 rd + 33963.7500 13.4396 18.6244 + 33963.7500 13.7542 18.8488 + 33963.7500 14.0687 19.0731 + 33963.7500 14.3833 19.2975 + 33963.7500 14.6979 19.5219 + 33963.7500 15.0125 19.7462 + 33963.7500 15.3271 19.9706 + 33963.7500 15.6417 20.1950 + 33963.7500 15.9562 20.4194 + 33963.7500 16.2708 20.6437 + 33963.7500 16.5854 20.8681 + 33963.7500 16.9000 21.0925 + 999.9990 999.9990 0.0000 + 66 22 2 37 34007.5000 + 34007.5000 0.6000 20.9650 + 34007.5000 0.9671 20.6045 + 34007.5000 1.3342 20.2440 + 34007.5000 1.7014 19.8834 + 34007.5000 2.0685 19.5229 + 34007.5000 2.4356 19.1624 + 34007.5000 2.8595 18.9125 + 34007.5000 3.3664 18.8231 + 34007.5000 3.8689 18.7155 + 34007.5000 4.3642 18.5766 + 34007.5000 4.8594 18.4377 + 34007.5000 5.3547 18.2989 + 34007.5000 5.8500 18.1600 rg + 34007.5000 6.4753 18.0192 + 34007.5000 7.1046 17.8925 + 34007.5000 7.7338 17.7658 + 34007.5000 8.3631 17.6391 + 34007.5000 8.9981 17.5650 + 34007.5000 9.6402 17.5650 + 34007.5000 10.2823 17.5650 + 34007.5000 10.9244 17.5650 + 34007.5000 11.5666 17.5650 + 34007.5000 12.2087 17.5650 + 34007.5000 12.7095 17.8928 + 34007.5000 13.1500 18.3600 rd + 34007.5000 13.4542 18.5846 + 34007.5000 13.7583 18.8092 + 34007.5000 14.0625 19.0338 + 34007.5000 14.3667 19.2583 + 34007.5000 14.6708 19.4829 + 34007.5000 14.9750 19.7075 + 34007.5000 15.2792 19.9321 + 34007.5000 15.5833 20.1567 + 34007.5000 15.8875 20.3813 + 34007.5000 16.1917 20.6058 + 34007.5000 16.4958 20.8304 + 34007.5000 16.8000 21.0550 + 999.9990 999.9990 0.0000 + 67 22 3 37 34051.2500 + 34051.2500 0.6000 20.9575 + 34051.2500 0.9687 20.5850 + 34051.2500 1.3374 20.2126 + 34051.2500 1.7061 19.8401 + 34051.2500 2.0748 19.4677 + 34051.2500 2.4435 19.0952 + 34051.2500 2.8663 18.8271 + 34051.2500 3.3831 18.7393 + 34051.2500 3.8967 18.6377 + 34051.2500 4.4038 18.5058 + 34051.2500 4.9109 18.3739 + 34051.2500 5.4179 18.2419 + 34051.2500 5.9250 18.1100 rg + 34051.2500 6.5472 17.9799 + 34051.2500 7.1728 17.8629 + 34051.2500 7.7984 17.7459 + 34051.2500 8.4239 17.6290 + 34051.2500 9.0532 17.5475 + 34051.2500 9.6898 17.5475 + 34051.2500 10.3264 17.5475 + 34051.2500 10.9630 17.5475 + 34051.2500 11.5996 17.5475 + 34051.2500 12.2362 17.5475 + 34051.2500 12.7394 17.8557 + 34051.2500 13.1750 18.3200 rd + 34051.2500 13.4688 18.5448 + 34051.2500 13.7625 18.7696 + 34051.2500 14.0563 18.9944 + 34051.2500 14.3500 19.2192 + 34051.2500 14.6438 19.4440 + 34051.2500 14.9375 19.6688 + 34051.2500 15.2312 19.8935 + 34051.2500 15.5250 20.1183 + 34051.2500 15.8187 20.3431 + 34051.2500 16.1125 20.5679 + 34051.2500 16.4062 20.7927 + 34051.2500 16.7000 21.0175 + 999.9990 999.9990 0.0000 + 68 23 0 37 34095.0000 + 34095.0000 0.6000 20.9500 + 34095.0000 0.9703 20.5656 + 34095.0000 1.3405 20.1812 + 34095.0000 1.7108 19.7969 + 34095.0000 2.0811 19.4125 + 34095.0000 2.4514 19.0281 + 34095.0000 2.8730 18.7417 + 34095.0000 3.3997 18.6555 + 34095.0000 3.9245 18.5600 + 34095.0000 4.4434 18.4350 + 34095.0000 4.9623 18.3100 + 34095.0000 5.4811 18.1850 + 34095.0000 6.0000 18.0600 rg + 34095.0000 6.6190 17.9405 + 34095.0000 7.2409 17.8333 + 34095.0000 7.8629 17.7261 + 34095.0000 8.4848 17.6188 + 34095.0000 9.1083 17.5300 + 34095.0000 9.7394 17.5300 + 34095.0000 10.3705 17.5300 + 34095.0000 11.0015 17.5300 + 34095.0000 11.6326 17.5300 + 34095.0000 12.2637 17.5300 + 34095.0000 12.7694 17.8186 + 34095.0000 13.2000 18.2800 rd + 34095.0000 13.4833 18.5050 + 34095.0000 13.7667 18.7300 + 34095.0000 14.0500 18.9550 + 34095.0000 14.3333 19.1800 + 34095.0000 14.6167 19.4050 + 34095.0000 14.9000 19.6300 + 34095.0000 15.1833 19.8550 + 34095.0000 15.4667 20.0800 + 34095.0000 15.7500 20.3050 + 34095.0000 16.0333 20.5300 + 34095.0000 16.3167 20.7550 + 34095.0000 16.6000 20.9800 + 999.9990 999.9990 0.0000 + 69 23 1 37 34145.0000 + 34145.0000 0.4500 21.0525 + 34145.0000 0.8418 20.6635 + 34145.0000 1.2166 20.2591 + 34145.0000 1.5837 19.8477 + 34145.0000 1.9508 19.4362 + 34145.0000 2.3179 19.0248 + 34145.0000 2.7235 18.6868 + 34145.0000 3.2478 18.5649 + 34145.0000 3.7920 18.4713 + 34145.0000 4.3315 18.3543 + 34145.0000 4.8710 18.2371 + 34145.0000 5.4105 18.1198 + 34145.0000 5.9500 18.0025 rg + 34145.0000 6.5600 17.8109 + 34145.0000 7.1979 17.7109 + 34145.0000 7.8422 17.6317 + 34145.0000 8.4866 17.5524 + 34145.0000 9.1321 17.4870 + 34145.0000 9.7833 17.4891 + 34145.0000 10.4345 17.4918 + 34145.0000 11.0857 17.4945 + 34145.0000 11.7369 17.4972 + 34145.0000 12.3881 17.4999 + 34145.0000 12.9228 17.8003 + 34145.0000 13.4000 18.2350 rd + 34145.0000 13.6562 18.4717 + 34145.0000 13.9125 18.7083 + 34145.0000 14.1688 18.9450 + 34145.0000 14.4250 19.1817 + 34145.0000 14.6813 19.4183 + 34145.0000 14.9375 19.6550 + 34145.0000 15.1938 19.8917 + 34145.0000 15.4500 20.1283 + 34145.0000 15.7063 20.3650 + 34145.0000 15.9625 20.6017 + 34145.0000 16.2188 20.8383 + 34145.0000 16.4750 21.0750 + 999.9990 999.9990 0.0000 + 70 23 2 37 34195.0000 + 34195.0000 0.3000 21.1550 + 34195.0000 0.7132 20.7613 + 34195.0000 1.0927 20.3369 + 34195.0000 1.4566 19.8984 + 34195.0000 1.8205 19.4599 + 34195.0000 2.1844 19.0214 + 34195.0000 2.5740 18.6319 + 34195.0000 3.0958 18.4742 + 34195.0000 3.6594 18.3827 + 34195.0000 4.2196 18.2737 + 34195.0000 4.7797 18.1641 + 34195.0000 5.3399 18.0546 + 34195.0000 5.9000 17.9450 rg + 34195.0000 6.5010 17.6812 + 34195.0000 7.1548 17.5885 + 34195.0000 7.8216 17.5373 + 34195.0000 8.4883 17.4861 + 34195.0000 9.1559 17.4440 + 34195.0000 9.8272 17.4481 + 34195.0000 10.4985 17.4535 + 34195.0000 11.1698 17.4589 + 34195.0000 11.8411 17.4643 + 34195.0000 12.5124 17.4697 + 34195.0000 13.0763 17.7820 + 34195.0000 13.6000 18.1900 rd + 34195.0000 13.8292 18.4383 + 34195.0000 14.0583 18.6867 + 34195.0000 14.2875 18.9350 + 34195.0000 14.5167 19.1833 + 34195.0000 14.7458 19.4317 + 34195.0000 14.9750 19.6800 + 34195.0000 15.2042 19.9283 + 34195.0000 15.4333 20.1767 + 34195.0000 15.6625 20.4250 + 34195.0000 15.8917 20.6733 + 34195.0000 16.1208 20.9217 + 34195.0000 16.3500 21.1700 + 999.9990 999.9990 0.0000 + 71 23 3 37 34245.0000 + 34245.0000 0.1500 21.2575 + 34245.0000 0.5847 20.8592 + 34245.0000 0.9688 20.4148 + 34245.0000 1.3295 19.9492 + 34245.0000 1.6902 19.4837 + 34245.0000 2.0510 19.0181 + 34245.0000 2.4246 18.5770 + 34245.0000 2.9439 18.3836 + 34245.0000 3.5268 18.2940 + 34245.0000 4.1077 18.1930 + 34245.0000 4.6885 18.0912 + 34245.0000 5.2692 17.9893 + 34245.0000 5.8500 17.8875 rg + 34245.0000 6.4420 17.5516 + 34245.0000 7.1118 17.4662 + 34245.0000 7.8009 17.4429 + 34245.0000 8.4901 17.4197 + 34245.0000 9.1797 17.4010 + 34245.0000 9.8711 17.4072 + 34245.0000 10.5625 17.4153 + 34245.0000 11.2539 17.4234 + 34245.0000 11.9453 17.4315 + 34245.0000 12.6367 17.4396 + 34245.0000 13.2297 17.7636 + 34245.0000 13.8000 18.1450 rd + 34245.0000 14.0021 18.4050 + 34245.0000 14.2042 18.6650 + 34245.0000 14.4062 18.9250 + 34245.0000 14.6083 19.1850 + 34245.0000 14.8104 19.4450 + 34245.0000 15.0125 19.7050 + 34245.0000 15.2146 19.9650 + 34245.0000 15.4167 20.2250 + 34245.0000 15.6188 20.4850 + 34245.0000 15.8208 20.7450 + 34245.0000 16.0229 21.0050 + 34245.0000 16.2250 21.2650 + 999.9990 999.9990 0.0000 + 72 24 0 37 34295.0000 + 34295.0000 0.0000 21.3600 + 34295.0000 0.4562 20.9570 + 34295.0000 0.8449 20.4926 + 34295.0000 1.2024 20.0000 + 34295.0000 1.5600 19.5074 + 34295.0000 1.9175 19.0148 + 34295.0000 2.2751 18.5221 + 34295.0000 2.7919 18.2930 + 34295.0000 3.3943 18.2054 + 34295.0000 3.9958 18.1124 + 34295.0000 4.5972 18.0183 + 34295.0000 5.1986 17.9241 + 34295.0000 5.8000 17.8300 rg + 34295.0000 6.3830 17.4219 + 34295.0000 7.0687 17.3438 + 34295.0000 7.7803 17.3485 + 34295.0000 8.4919 17.3533 + 34295.0000 9.2035 17.3580 + 34295.0000 9.9150 17.3663 + 34295.0000 10.6265 17.3771 + 34295.0000 11.3380 17.3878 + 34295.0000 12.0495 17.3986 + 34295.0000 12.7610 17.4094 + 34295.0000 13.3831 17.7453 + 34295.0000 14.0000 18.1000 rd + 34295.0000 14.1750 18.3717 + 34295.0000 14.3500 18.6433 + 34295.0000 14.5250 18.9150 + 34295.0000 14.7000 19.1867 + 34295.0000 14.8750 19.4583 + 34295.0000 15.0500 19.7300 + 34295.0000 15.2250 20.0017 + 34295.0000 15.4000 20.2733 + 34295.0000 15.5750 20.5450 + 34295.0000 15.7500 20.8167 + 34295.0000 15.9250 21.0883 + 34295.0000 16.1000 21.3600 + 999.9990 999.9990 0.0000 + 73 25 0 37 34334.0000 amontPontT + 34334.0000 0.0000 21.3600 + 34334.0000 0.4562 20.9570 + 34334.0000 0.8449 20.4926 + 34334.0000 1.2024 20.0000 + 34334.0000 1.5600 19.5074 + 34334.0000 1.9175 19.0148 + 34334.0000 2.2751 18.5221 + 34334.0000 2.7919 18.2930 + 34334.0000 3.3943 18.2054 + 34334.0000 3.9958 18.1124 + 34334.0000 4.5972 18.0183 + 34334.0000 5.1986 17.9241 + 34334.0000 5.8000 17.8300 rg + 34334.0000 6.3830 17.4219 + 34334.0000 7.0687 17.3438 + 34334.0000 7.7803 17.3485 + 34334.0000 8.4919 17.3533 + 34334.0000 9.2035 17.3580 + 34334.0000 9.9150 17.3663 + 34334.0000 10.6265 17.3771 + 34334.0000 11.3380 17.3878 + 34334.0000 12.0495 17.3986 + 34334.0000 12.7610 17.4094 + 34334.0000 13.3831 17.7453 + 34334.0000 14.0000 18.1000 rd + 34334.0000 14.1750 18.3525 + 34334.0000 14.3500 18.6050 + 34334.0000 14.5250 18.8575 + 34334.0000 14.7000 19.1100 + 34334.0000 14.8750 19.3625 + 34334.0000 15.0500 19.6150 + 34334.0000 15.2250 19.8675 + 34334.0000 15.4000 20.1200 + 34334.0000 15.5750 20.3725 + 34334.0000 15.7500 20.6250 + 34334.0000 15.9250 20.8775 + 34334.0000 16.1000 21.1300 + 999.9990 999.9990 0.0000 + 74 26 0 37 34335.0000 PontmThive + 34335.0000 2.9000 20.7400 + 34335.0000 2.9000 20.5358 + 34335.0000 2.9000 20.3317 + 34335.0000 2.9000 20.1275 + 34335.0000 2.9000 19.9233 + 34335.0000 2.9000 19.7192 + 34335.0000 2.9000 19.5150 + 34335.0000 2.9000 19.3108 + 34335.0000 2.9000 19.1067 + 34335.0000 2.9000 18.9025 + 34335.0000 2.9000 18.6983 + 34335.0000 2.9000 18.4942 + 34335.0000 2.9000 18.2900 rg + 34335.0000 3.8099 18.1600 + 34335.0000 4.7197 18.0300 + 34335.0000 5.6296 17.9001 + 34335.0000 6.5389 17.7670 + 34335.0000 7.4457 17.6168 + 34335.0000 8.3524 17.4666 + 34335.0000 9.2592 17.3164 + 34335.0000 10.1736 17.2600 + 34335.0000 11.0927 17.2600 + 34335.0000 12.0118 17.2600 + 34335.0000 12.8000 17.3909 + 34335.0000 12.8000 18.3100 rd + 34335.0000 12.8000 18.5125 + 34335.0000 12.8000 18.7150 + 34335.0000 12.8000 18.9175 + 34335.0000 12.8000 19.1200 + 34335.0000 12.8000 19.3225 + 34335.0000 12.8000 19.5250 + 34335.0000 12.8000 19.7275 + 34335.0000 12.8000 19.9300 + 34335.0000 12.8000 20.1325 + 34335.0000 12.8000 20.3350 + 34335.0000 12.8000 20.5375 + 34335.0000 12.8000 20.7400 + 999.9990 999.9990 0.0000 + 75 27 0 37 34343.0000 PontvThive + 34343.0000 2.9000 20.7400 + 34343.0000 2.9000 20.5358 + 34343.0000 2.9000 20.3317 + 34343.0000 2.9000 20.1275 + 34343.0000 2.9000 19.9233 + 34343.0000 2.9000 19.7192 + 34343.0000 2.9000 19.5150 + 34343.0000 2.9000 19.3108 + 34343.0000 2.9000 19.1067 + 34343.0000 2.9000 18.9025 + 34343.0000 2.9000 18.6983 + 34343.0000 2.9000 18.4942 + 34343.0000 2.9000 18.2900 rg + 34343.0000 3.8082 18.1603 + 34343.0000 4.7164 18.0305 + 34343.0000 5.6246 17.9008 + 34343.0000 6.5324 17.7681 + 34343.0000 7.4375 17.6182 + 34343.0000 8.3426 17.4683 + 34343.0000 9.2477 17.3184 + 34343.0000 10.1603 17.2600 + 34343.0000 11.0777 17.2600 + 34343.0000 11.9951 17.2600 + 34343.0000 12.8000 17.3726 + 34343.0000 12.8000 18.2900 rd + 34343.0000 12.8000 18.4942 + 34343.0000 12.8000 18.6983 + 34343.0000 12.8000 18.9025 + 34343.0000 12.8000 19.1067 + 34343.0000 12.8000 19.3108 + 34343.0000 12.8000 19.5150 + 34343.0000 12.8000 19.7192 + 34343.0000 12.8000 19.9233 + 34343.0000 12.8000 20.1275 + 34343.0000 12.8000 20.3317 + 34343.0000 12.8000 20.5358 + 34343.0000 12.8000 20.7400 + 999.9990 999.9990 0.0000 + 76 28 0 37 34345.0000 avalPontTh + 34345.0000 0.0000 21.3600 + 34345.0000 0.4562 20.9570 + 34345.0000 0.8449 20.4926 + 34345.0000 1.2024 20.0000 + 34345.0000 1.5600 19.5074 + 34345.0000 1.9175 19.0148 + 34345.0000 2.2751 18.5221 + 34345.0000 2.7919 18.2930 + 34345.0000 3.3943 18.2054 + 34345.0000 3.9958 18.1124 + 34345.0000 4.5972 18.0183 + 34345.0000 5.1986 17.9241 + 34345.0000 5.8000 17.8300 rg + 34345.0000 6.3832 17.4217 + 34345.0000 7.0692 17.3248 + 34345.0000 7.7809 17.3058 + 34345.0000 8.4926 17.2869 + 34345.0000 9.2042 17.2679 + 34345.0000 9.9156 17.2789 + 34345.0000 10.6268 17.3112 + 34345.0000 11.3380 17.3435 + 34345.0000 12.0492 17.3759 + 34345.0000 12.7604 17.4082 + 34345.0000 13.3828 17.7451 + 34345.0000 14.0000 18.1000 rd + 34345.0000 14.1750 18.3525 + 34345.0000 14.3500 18.6050 + 34345.0000 14.5250 18.8575 + 34345.0000 14.7000 19.1100 + 34345.0000 14.8750 19.3625 + 34345.0000 15.0500 19.6150 + 34345.0000 15.2250 19.8675 + 34345.0000 15.4000 20.1200 + 34345.0000 15.5750 20.3725 + 34345.0000 15.7500 20.6250 + 34345.0000 15.9250 20.8775 + 34345.0000 16.1000 21.1300 + 999.9990 999.9990 0.0000 + 77 29 0 37 34405.0000 + 34405.0000 0.0000 21.1400 + 34405.0000 0.2417 20.8650 + 34405.0000 0.4833 20.5900 + 34405.0000 0.7250 20.3150 + 34405.0000 0.9667 20.0400 + 34405.0000 1.2083 19.7650 + 34405.0000 1.4500 19.4900 + 34405.0000 1.6917 19.2150 + 34405.0000 1.9333 18.9400 + 34405.0000 2.1750 18.6650 + 34405.0000 2.4167 18.3900 + 34405.0000 2.6583 18.1150 + 34405.0000 2.9000 17.8400 rg + 34405.0000 3.8268 17.7341 + 34405.0000 4.7537 17.6281 + 34405.0000 5.6805 17.5222 + 34405.0000 6.6081 17.4244 + 34405.0000 7.5384 17.3546 + 34405.0000 8.4687 17.2848 + 34405.0000 9.3989 17.2151 + 34405.0000 10.3312 17.2046 + 34405.0000 11.2641 17.2104 + 34405.0000 12.1970 17.2162 + 34405.0000 13.0873 17.3819 + 34405.0000 13.9000 17.8400 rd + 34405.0000 14.3250 18.1033 + 34405.0000 14.7500 18.3667 + 34405.0000 15.1750 18.6300 + 34405.0000 15.6000 18.8933 + 34405.0000 16.0250 19.1567 + 34405.0000 16.4500 19.4200 + 34405.0000 16.8750 19.6833 + 34405.0000 17.3000 19.9467 + 34405.0000 17.7250 20.2100 + 34405.0000 18.1500 20.4733 + 34405.0000 18.5750 20.7367 + 34405.0000 19.0000 21.0000 + 999.9990 999.9990 0.0000 + 78 29 1 37 34448.3333 + 34448.3333 0.0000 20.9567 + 34448.3333 0.2417 20.6942 + 34448.3333 0.4833 20.4317 + 34448.3333 0.7250 20.1692 + 34448.3333 0.9667 19.9067 + 34448.3333 1.2083 19.6442 + 34448.3333 1.4500 19.3817 + 34448.3333 1.6917 19.1192 + 34448.3333 1.9333 18.8567 + 34448.3333 2.1750 18.5942 + 34448.3333 2.4167 18.3317 + 34448.3333 2.6583 18.0692 + 34448.3333 2.9000 17.8067 rg + 34448.3333 3.8187 17.6931 + 34448.3333 4.7374 17.5795 + 34448.3333 5.6561 17.4659 + 34448.3333 6.5757 17.3605 + 34448.3333 7.4991 17.2941 + 34448.3333 8.4225 17.2277 + 34448.3333 9.3459 17.1613 + 34448.3333 10.2710 17.1464 + 34448.3333 11.1968 17.1503 + 34448.3333 12.1226 17.1542 + 34448.3333 13.0115 17.2849 + 34448.3333 13.7667 17.8067 rd + 34448.3333 14.1139 18.0617 + 34448.3333 14.4611 18.3167 + 34448.3333 14.8083 18.5717 + 34448.3333 15.1556 18.8267 + 34448.3333 15.5028 19.0817 + 34448.3333 15.8500 19.3367 + 34448.3333 16.1972 19.5917 + 34448.3333 16.5444 19.8467 + 34448.3333 16.8917 20.1017 + 34448.3333 17.2389 20.3567 + 34448.3333 17.5861 20.6117 + 34448.3333 17.9333 20.8667 + 999.9990 999.9990 0.0000 + 79 29 2 37 34491.6667 + 34491.6667 0.0000 20.7733 + 34491.6667 0.2417 20.5233 + 34491.6667 0.4833 20.2733 + 34491.6667 0.7250 20.0233 + 34491.6667 0.9667 19.7733 + 34491.6667 1.2083 19.5233 + 34491.6667 1.4500 19.2733 + 34491.6667 1.6917 19.0233 + 34491.6667 1.9333 18.7733 + 34491.6667 2.1750 18.5233 + 34491.6667 2.4167 18.2733 + 34491.6667 2.6583 18.0233 + 34491.6667 2.9000 17.7733 rg + 34491.6667 3.8106 17.6521 + 34491.6667 4.7212 17.5308 + 34491.6667 5.6318 17.4096 + 34491.6667 6.5432 17.2966 + 34491.6667 7.4597 17.2336 + 34491.6667 8.3763 17.1705 + 34491.6667 9.2928 17.1075 + 34491.6667 10.2108 17.0882 + 34491.6667 11.1295 17.0901 + 34491.6667 12.0482 17.0921 + 34491.6667 12.9358 17.1879 + 34491.6667 13.6333 17.7733 rd + 34491.6667 13.9028 18.0200 + 34491.6667 14.1722 18.2667 + 34491.6667 14.4417 18.5133 + 34491.6667 14.7111 18.7600 + 34491.6667 14.9806 19.0067 + 34491.6667 15.2500 19.2533 + 34491.6667 15.5194 19.5000 + 34491.6667 15.7889 19.7467 + 34491.6667 16.0583 19.9933 + 34491.6667 16.3278 20.2400 + 34491.6667 16.5972 20.4867 + 34491.6667 16.8667 20.7333 + 999.9990 999.9990 0.0000 + 80 30 0 37 34535.0000 + 34535.0000 0.0000 20.5900 + 34535.0000 0.2417 20.3525 + 34535.0000 0.4833 20.1150 + 34535.0000 0.7250 19.8775 + 34535.0000 0.9667 19.6400 + 34535.0000 1.2083 19.4025 + 34535.0000 1.4500 19.1650 + 34535.0000 1.6917 18.9275 + 34535.0000 1.9333 18.6900 + 34535.0000 2.1750 18.4525 + 34535.0000 2.4167 18.2150 + 34535.0000 2.6583 17.9775 + 34535.0000 2.9000 17.7400 rg + 34535.0000 3.8025 17.6111 + 34535.0000 4.7049 17.4822 + 34535.0000 5.6074 17.3532 + 34535.0000 6.5107 17.2327 + 34535.0000 7.4204 17.1730 + 34535.0000 8.3300 17.1133 + 34535.0000 9.2397 17.0536 + 34535.0000 10.1506 17.0300 + 34535.0000 11.0622 17.0300 + 34535.0000 11.9738 17.0300 + 34535.0000 12.8600 17.0908 + 34535.0000 13.5000 17.7400 rd + 34535.0000 13.6917 17.9783 + 34535.0000 13.8833 18.2167 + 34535.0000 14.0750 18.4550 + 34535.0000 14.2667 18.6933 + 34535.0000 14.4583 18.9317 + 34535.0000 14.6500 19.1700 + 34535.0000 14.8417 19.4083 + 34535.0000 15.0333 19.6467 + 34535.0000 15.2250 19.8850 + 34535.0000 15.4167 20.1233 + 34535.0000 15.6083 20.3617 + 34535.0000 15.8000 20.6000 + 999.9990 999.9990 0.0000 + 81 30 1 37 34591.6667 + 34591.6667 0.0000 20.5600 + 34591.6667 0.2417 20.3214 + 34591.6667 0.4833 20.0828 + 34591.6667 0.7250 19.8442 + 34591.6667 0.9667 19.6056 + 34591.6667 1.2083 19.3669 + 34591.6667 1.4500 19.1283 + 34591.6667 1.6917 18.8897 + 34591.6667 1.9333 18.6511 + 34591.6667 2.1750 18.4125 + 34591.6667 2.4167 18.1739 + 34591.6667 2.6583 17.9353 + 34591.6667 2.9000 17.6967 rg + 34591.6667 3.8208 17.5651 + 34591.6667 4.7417 17.4336 + 34591.6667 5.6625 17.3020 + 34591.6667 6.5847 17.1828 + 34591.6667 7.5126 17.1169 + 34591.6667 8.4404 17.0510 + 34591.6667 9.3683 16.9858 + 34591.6667 10.2979 16.9700 + 34591.6667 11.2281 16.9700 + 34591.6667 12.1583 16.9700 + 34591.6667 13.0072 17.1708 + 34591.6667 13.6667 17.8267 rd + 34591.6667 13.8556 18.0558 + 34591.6667 14.0444 18.2850 + 34591.6667 14.2333 18.5142 + 34591.6667 14.4222 18.7433 + 34591.6667 14.6111 18.9725 + 34591.6667 14.8000 19.2017 + 34591.6667 14.9889 19.4308 + 34591.6667 15.1778 19.6600 + 34591.6667 15.3667 19.8892 + 34591.6667 15.5556 20.1183 + 34591.6667 15.7444 20.3475 + 34591.6667 15.9333 20.5767 + 999.9990 999.9990 0.0000 + 82 30 2 37 34648.3333 + 34648.3333 0.0000 20.5300 + 34648.3333 0.2417 20.2903 + 34648.3333 0.4833 20.0506 + 34648.3333 0.7250 19.8108 + 34648.3333 0.9667 19.5711 + 34648.3333 1.2083 19.3314 + 34648.3333 1.4500 19.0917 + 34648.3333 1.6917 18.8519 + 34648.3333 1.9333 18.6122 + 34648.3333 2.1750 18.3725 + 34648.3333 2.4167 18.1328 + 34648.3333 2.6583 17.8931 + 34648.3333 2.9000 17.6533 rg + 34648.3333 3.8392 17.5192 + 34648.3333 4.7785 17.3850 + 34648.3333 5.7177 17.2508 + 34648.3333 6.6587 17.1329 + 34648.3333 7.6047 17.0608 + 34648.3333 8.5507 16.9886 + 34648.3333 9.4968 16.9179 + 34648.3333 10.4453 16.9100 + 34648.3333 11.3941 16.9100 + 34648.3333 12.3429 16.9100 + 34648.3333 13.1544 17.2507 + 34648.3333 13.8333 17.9133 rd + 34648.3333 14.0194 18.1333 + 34648.3333 14.2056 18.3533 + 34648.3333 14.3917 18.5733 + 34648.3333 14.5778 18.7933 + 34648.3333 14.7639 19.0133 + 34648.3333 14.9500 19.2333 + 34648.3333 15.1361 19.4533 + 34648.3333 15.3222 19.6733 + 34648.3333 15.5083 19.8933 + 34648.3333 15.6944 20.1133 + 34648.3333 15.8806 20.3333 + 34648.3333 16.0667 20.5533 + 999.9990 999.9990 0.0000 + 83 31 0 37 34705.0000 + 34705.0000 0.0000 20.5000 + 34705.0000 0.2417 20.2592 + 34705.0000 0.4833 20.0183 + 34705.0000 0.7250 19.7775 + 34705.0000 0.9667 19.5367 + 34705.0000 1.2083 19.2958 + 34705.0000 1.4500 19.0550 + 34705.0000 1.6917 18.8142 + 34705.0000 1.9333 18.5733 + 34705.0000 2.1750 18.3325 + 34705.0000 2.4167 18.0917 + 34705.0000 2.6583 17.8508 + 34705.0000 2.9000 17.6100 rg + 34705.0000 3.8576 17.4732 + 34705.0000 4.8152 17.3364 + 34705.0000 5.7729 17.1996 + 34705.0000 6.7328 17.0830 + 34705.0000 7.6969 17.0046 + 34705.0000 8.6611 16.9263 + 34705.0000 9.6253 16.8500 + 34705.0000 10.5927 16.8500 + 34705.0000 11.5600 16.8500 + 34705.0000 12.5274 16.8500 + 34705.0000 13.3016 17.3307 + 34705.0000 14.0000 18.0000 rd + 34705.0000 14.1833 18.2108 + 34705.0000 14.3667 18.4217 + 34705.0000 14.5500 18.6325 + 34705.0000 14.7333 18.8433 + 34705.0000 14.9167 19.0542 + 34705.0000 15.1000 19.2650 + 34705.0000 15.2833 19.4758 + 34705.0000 15.4667 19.6867 + 34705.0000 15.6500 19.8975 + 34705.0000 15.8333 20.1083 + 34705.0000 16.0167 20.3192 + 34705.0000 16.2000 20.5300 + 999.9990 999.9990 0.0000 + 84 31 1 37 34765.0000 + 34765.0000 0.0000 20.5600 + 34765.0000 0.2417 20.3104 + 34765.0000 0.4833 20.0608 + 34765.0000 0.7250 19.8113 + 34765.0000 0.9667 19.5617 + 34765.0000 1.2083 19.3121 + 34765.0000 1.4500 19.0625 + 34765.0000 1.6917 18.8129 + 34765.0000 1.9333 18.5633 + 34765.0000 2.1750 18.3137 + 34765.0000 2.4167 18.0642 + 34765.0000 2.6583 17.8146 + 34765.0000 2.9000 17.5650 rg + 34765.0000 3.8334 17.4317 + 34765.0000 4.7668 17.2983 + 34765.0000 5.7002 17.1650 + 34765.0000 6.6351 17.0448 + 34765.0000 7.5743 16.9614 + 34765.0000 8.5134 16.8779 + 34765.0000 9.4526 16.7955 + 34765.0000 10.3948 16.7800 + 34765.0000 11.3377 16.7800 + 34765.0000 12.2805 16.7800 + 34765.0000 13.1005 17.0779 + 34765.0000 13.7500 17.7600 rd + 34765.0000 13.9542 17.9896 + 34765.0000 14.1583 18.2192 + 34765.0000 14.3625 18.4488 + 34765.0000 14.5667 18.6783 + 34765.0000 14.7708 18.9079 + 34765.0000 14.9750 19.1375 + 34765.0000 15.1792 19.3671 + 34765.0000 15.3833 19.5967 + 34765.0000 15.5875 19.8263 + 34765.0000 15.7917 20.0558 + 34765.0000 15.9958 20.2854 + 34765.0000 16.2000 20.5150 + 999.9990 999.9990 0.0000 + 85 32 0 37 34825.0000 + 34825.0000 0.0000 20.6200 + 34825.0000 0.2417 20.3617 + 34825.0000 0.4833 20.1033 + 34825.0000 0.7250 19.8450 + 34825.0000 0.9667 19.5867 + 34825.0000 1.2083 19.3283 + 34825.0000 1.4500 19.0700 + 34825.0000 1.6917 18.8117 + 34825.0000 1.9333 18.5533 + 34825.0000 2.1750 18.2950 + 34825.0000 2.4167 18.0367 + 34825.0000 2.6583 17.7783 + 34825.0000 2.9000 17.5200 rg + 34825.0000 3.8092 17.3901 + 34825.0000 4.7184 17.2602 + 34825.0000 5.6276 17.1303 + 34825.0000 6.5375 17.0067 + 34825.0000 7.4517 16.9181 + 34825.0000 8.3658 16.8296 + 34825.0000 9.2799 16.7410 + 34825.0000 10.1969 16.7100 + 34825.0000 11.1153 16.7100 + 34825.0000 12.0337 16.7100 + 34825.0000 12.8995 16.8251 + 34825.0000 13.5000 17.5200 rd + 34825.0000 13.7250 17.7683 + 34825.0000 13.9500 18.0167 + 34825.0000 14.1750 18.2650 + 34825.0000 14.4000 18.5133 + 34825.0000 14.6250 18.7617 + 34825.0000 14.8500 19.0100 + 34825.0000 15.0750 19.2583 + 34825.0000 15.3000 19.5067 + 34825.0000 15.5250 19.7550 + 34825.0000 15.7500 20.0033 + 34825.0000 15.9750 20.2517 + 34825.0000 16.2000 20.5000 + 999.9990 999.9990 0.0000 + 86 32 1 37 34881.6667 + 34881.6667 0.0000 20.5433 + 34881.6667 0.2417 20.2878 + 34881.6667 0.4833 20.0322 + 34881.6667 0.7250 19.7767 + 34881.6667 0.9667 19.5211 + 34881.6667 1.2083 19.2656 + 34881.6667 1.4500 19.0100 + 34881.6667 1.6917 18.7544 + 34881.6667 1.9333 18.4989 + 34881.6667 2.1750 18.2433 + 34881.6667 2.4167 17.9878 + 34881.6667 2.6583 17.7322 + 34881.6667 2.9000 17.4767 rg + 34881.6667 3.8130 17.3462 + 34881.6667 4.7260 17.2158 + 34881.6667 5.6390 17.0854 + 34881.6667 6.5528 16.9609 + 34881.6667 7.4702 16.8672 + 34881.6667 8.3877 16.7735 + 34881.6667 9.3052 16.6798 + 34881.6667 10.2260 16.6500 + 34881.6667 11.1483 16.6500 + 34881.6667 12.0705 16.6500 + 34881.6667 12.9270 16.7950 + 34881.6667 13.5333 17.4900 rd + 34881.6667 13.7556 17.7358 + 34881.6667 13.9778 17.9817 + 34881.6667 14.2000 18.2275 + 34881.6667 14.4222 18.4733 + 34881.6667 14.6444 18.7192 + 34881.6667 14.8667 18.9650 + 34881.6667 15.0889 19.2108 + 34881.6667 15.3111 19.4567 + 34881.6667 15.5333 19.7025 + 34881.6667 15.7556 19.9483 + 34881.6667 15.9778 20.1942 + 34881.6667 16.2000 20.4400 + 999.9990 999.9990 0.0000 + 87 32 2 37 34938.3333 + 34938.3333 0.0000 20.4667 + 34938.3333 0.2417 20.2139 + 34938.3333 0.4833 19.9611 + 34938.3333 0.7250 19.7083 + 34938.3333 0.9667 19.4556 + 34938.3333 1.2083 19.2028 + 34938.3333 1.4500 18.9500 + 34938.3333 1.6917 18.6972 + 34938.3333 1.9333 18.4444 + 34938.3333 2.1750 18.1917 + 34938.3333 2.4167 17.9389 + 34938.3333 2.6583 17.6861 + 34938.3333 2.9000 17.4333 rg + 34938.3333 3.8168 17.3024 + 34938.3333 4.7337 17.1714 + 34938.3333 5.6505 17.0404 + 34938.3333 6.5680 16.9151 + 34938.3333 7.4888 16.8163 + 34938.3333 8.4097 16.7175 + 34938.3333 9.3305 16.6187 + 34938.3333 10.2551 16.5900 + 34938.3333 11.1812 16.5900 + 34938.3333 12.1074 16.5900 + 34938.3333 12.9546 16.7650 + 34938.3333 13.5667 17.4600 rd + 34938.3333 13.7861 17.7033 + 34938.3333 14.0056 17.9467 + 34938.3333 14.2250 18.1900 + 34938.3333 14.4444 18.4333 + 34938.3333 14.6639 18.6767 + 34938.3333 14.8833 18.9200 + 34938.3333 15.1028 19.1633 + 34938.3333 15.3222 19.4067 + 34938.3333 15.5417 19.6500 + 34938.3333 15.7611 19.8933 + 34938.3333 15.9806 20.1367 + 34938.3333 16.2000 20.3800 + 999.9990 999.9990 0.0000 + 88 33 0 37 34995.0000 + 34995.0000 0.0000 20.3900 + 34995.0000 0.2417 20.1400 + 34995.0000 0.4833 19.8900 + 34995.0000 0.7250 19.6400 + 34995.0000 0.9667 19.3900 + 34995.0000 1.2083 19.1400 + 34995.0000 1.4500 18.8900 + 34995.0000 1.6917 18.6400 + 34995.0000 1.9333 18.3900 + 34995.0000 2.1750 18.1400 + 34995.0000 2.4167 17.8900 + 34995.0000 2.6583 17.6400 + 34995.0000 2.9000 17.3900 rg + 34995.0000 3.8206 17.2585 + 34995.0000 4.7413 17.1270 + 34995.0000 5.6619 16.9954 + 34995.0000 6.5833 16.8694 + 34995.0000 7.5074 16.7654 + 34995.0000 8.4316 16.6614 + 34995.0000 9.3558 16.5575 + 34995.0000 10.2842 16.5300 + 34995.0000 11.2142 16.5300 + 34995.0000 12.1442 16.5300 + 34995.0000 12.9821 16.7349 + 34995.0000 13.6000 17.4300 rd + 34995.0000 13.8167 17.6708 + 34995.0000 14.0333 17.9117 + 34995.0000 14.2500 18.1525 + 34995.0000 14.4667 18.3933 + 34995.0000 14.6833 18.6342 + 34995.0000 14.9000 18.8750 + 34995.0000 15.1167 19.1158 + 34995.0000 15.3333 19.3567 + 34995.0000 15.5500 19.5975 + 34995.0000 15.7667 19.8383 + 34995.0000 15.9833 20.0792 + 34995.0000 16.2000 20.3200 + 999.9990 999.9990 0.0000 + 89 33 1 37 35041.6667 + 35041.6667 0.0000 20.3133 + 35041.6667 0.2417 20.0669 + 35041.6667 0.4833 19.8206 + 35041.6667 0.7250 19.5742 + 35041.6667 0.9667 19.3278 + 35041.6667 1.2083 19.0814 + 35041.6667 1.4500 18.8350 + 35041.6667 1.6917 18.5886 + 35041.6667 1.9333 18.3422 + 35041.6667 2.1750 18.0958 + 35041.6667 2.4167 17.8494 + 35041.6667 2.6583 17.6031 + 35041.6667 2.9000 17.3567 rg + 35041.6667 3.8273 17.2242 + 35041.6667 4.7546 17.0917 + 35041.6667 5.6820 16.9592 + 35041.6667 6.6101 16.8330 + 35041.6667 7.5409 16.7283 + 35041.6667 8.4718 16.6236 + 35041.6667 9.4027 16.5189 + 35041.6667 10.3381 16.4967 + 35041.6667 11.2749 16.4967 + 35041.6667 12.2116 16.4967 + 35041.6667 13.0473 16.7408 + 35041.6667 13.7000 17.4100 rd + 35041.6667 13.9083 17.6453 + 35041.6667 14.1167 17.8806 + 35041.6667 14.3250 18.1158 + 35041.6667 14.5333 18.3511 + 35041.6667 14.7417 18.5864 + 35041.6667 14.9500 18.8217 + 35041.6667 15.1583 19.0569 + 35041.6667 15.3667 19.2922 + 35041.6667 15.5750 19.5275 + 35041.6667 15.7833 19.7628 + 35041.6667 15.9917 19.9981 + 35041.6667 16.2000 20.2333 + 999.9990 999.9990 0.0000 + 90 33 2 37 35088.3333 + 35088.3333 0.0000 20.2367 + 35088.3333 0.2417 19.9939 + 35088.3333 0.4833 19.7511 + 35088.3333 0.7250 19.5083 + 35088.3333 0.9667 19.2656 + 35088.3333 1.2083 19.0228 + 35088.3333 1.4500 18.7800 + 35088.3333 1.6917 18.5372 + 35088.3333 1.9333 18.2944 + 35088.3333 2.1750 18.0517 + 35088.3333 2.4167 17.8089 + 35088.3333 2.6583 17.5661 + 35088.3333 2.9000 17.3233 rg + 35088.3333 3.8340 17.1899 + 35088.3333 4.7680 17.0565 + 35088.3333 5.7020 16.9230 + 35088.3333 6.6369 16.7967 + 35088.3333 7.5744 16.6912 + 35088.3333 8.5120 16.5857 + 35088.3333 9.4496 16.4803 + 35088.3333 10.3921 16.4633 + 35088.3333 11.3356 16.4633 + 35088.3333 12.2791 16.4633 + 35088.3333 13.1125 16.7468 + 35088.3333 13.8000 17.3900 rd + 35088.3333 14.0000 17.6197 + 35088.3333 14.2000 17.8494 + 35088.3333 14.4000 18.0792 + 35088.3333 14.6000 18.3089 + 35088.3333 14.8000 18.5386 + 35088.3333 15.0000 18.7683 + 35088.3333 15.2000 18.9981 + 35088.3333 15.4000 19.2278 + 35088.3333 15.6000 19.4575 + 35088.3333 15.8000 19.6872 + 35088.3333 16.0000 19.9169 + 35088.3333 16.2000 20.1467 + 999.9990 999.9990 0.0000 + 91 34 0 37 35135.0000 + 35135.0000 0.0000 20.1600 + 35135.0000 0.2417 19.9208 + 35135.0000 0.4833 19.6817 + 35135.0000 0.7250 19.4425 + 35135.0000 0.9667 19.2033 + 35135.0000 1.2083 18.9642 + 35135.0000 1.4500 18.7250 + 35135.0000 1.6917 18.4858 + 35135.0000 1.9333 18.2467 + 35135.0000 2.1750 18.0075 + 35135.0000 2.4167 17.7683 + 35135.0000 2.6583 17.5292 + 35135.0000 2.9000 17.2900 rg + 35135.0000 3.8407 17.1556 + 35135.0000 4.7813 17.0212 + 35135.0000 5.7220 16.8869 + 35135.0000 6.6637 16.7603 + 35135.0000 7.6079 16.6541 + 35135.0000 8.5522 16.5479 + 35135.0000 9.4965 16.4416 + 35135.0000 10.4460 16.4300 + 35135.0000 11.3963 16.4300 + 35135.0000 12.3465 16.4300 + 35135.0000 13.1776 16.7527 + 35135.0000 13.9000 17.3700 rd + 35135.0000 14.0917 17.5942 + 35135.0000 14.2833 17.8183 + 35135.0000 14.4750 18.0425 + 35135.0000 14.6667 18.2667 + 35135.0000 14.8583 18.4908 + 35135.0000 15.0500 18.7150 + 35135.0000 15.2417 18.9392 + 35135.0000 15.4333 19.1633 + 35135.0000 15.6250 19.3875 + 35135.0000 15.8167 19.6117 + 35135.0000 16.0083 19.8358 + 35135.0000 16.2000 20.0600 + 999.9990 999.9990 0.0000 + 92 34 1 37 35180.0000 + 35180.0000 0.0000 20.1000 + 35180.0000 0.2417 19.8630 + 35180.0000 0.4833 19.6260 + 35180.0000 0.7250 19.3890 + 35180.0000 0.9667 19.1520 + 35180.0000 1.2083 18.9150 + 35180.0000 1.4500 18.6780 + 35180.0000 1.6917 18.4410 + 35180.0000 1.9333 18.2040 + 35180.0000 2.1750 17.9670 + 35180.0000 2.4167 17.7300 + 35180.0000 2.6583 17.4930 + 35180.0000 2.9000 17.2560 rg + 35180.0000 3.8373 17.1221 + 35180.0000 4.7747 16.9882 + 35180.0000 5.7120 16.8543 + 35180.0000 6.6503 16.7278 + 35180.0000 7.5912 16.6220 + 35180.0000 8.5321 16.5161 + 35180.0000 9.4730 16.4103 + 35180.0000 10.4191 16.3960 + 35180.0000 11.3660 16.3960 + 35180.0000 12.3128 16.3960 + 35180.0000 13.1471 16.6972 + 35180.0000 13.8600 17.3200 rd + 35180.0000 14.0550 17.5383 + 35180.0000 14.2500 17.7567 + 35180.0000 14.4450 17.9750 + 35180.0000 14.6400 18.1933 + 35180.0000 14.8350 18.4117 + 35180.0000 15.0300 18.6300 + 35180.0000 15.2250 18.8483 + 35180.0000 15.4200 19.0667 + 35180.0000 15.6150 19.2850 + 35180.0000 15.8100 19.5033 + 35180.0000 16.0050 19.7217 + 35180.0000 16.2000 19.9400 + 999.9990 999.9990 0.0000 + 93 34 2 37 35225.0000 + 35225.0000 0.0000 20.0400 + 35225.0000 0.2417 19.8052 + 35225.0000 0.4833 19.5703 + 35225.0000 0.7250 19.3355 + 35225.0000 0.9667 19.1007 + 35225.0000 1.2083 18.8658 + 35225.0000 1.4500 18.6310 + 35225.0000 1.6917 18.3962 + 35225.0000 1.9333 18.1613 + 35225.0000 2.1750 17.9265 + 35225.0000 2.4167 17.6917 + 35225.0000 2.6583 17.4568 + 35225.0000 2.9000 17.2220 rg + 35225.0000 3.8340 17.0886 + 35225.0000 4.7680 16.9551 + 35225.0000 5.7020 16.8217 + 35225.0000 6.6369 16.6953 + 35225.0000 7.5745 16.5899 + 35225.0000 8.5120 16.4844 + 35225.0000 9.4496 16.3789 + 35225.0000 10.3922 16.3620 + 35225.0000 11.3356 16.3620 + 35225.0000 12.2791 16.3620 + 35225.0000 13.1166 16.6417 + 35225.0000 13.8200 17.2700 rd + 35225.0000 14.0183 17.4825 + 35225.0000 14.2167 17.6950 + 35225.0000 14.4150 17.9075 + 35225.0000 14.6133 18.1200 + 35225.0000 14.8117 18.3325 + 35225.0000 15.0100 18.5450 + 35225.0000 15.2083 18.7575 + 35225.0000 15.4067 18.9700 + 35225.0000 15.6050 19.1825 + 35225.0000 15.8033 19.3950 + 35225.0000 16.0017 19.6075 + 35225.0000 16.2000 19.8200 + 999.9990 999.9990 0.0000 + 94 34 3 37 35270.0000 + 35270.0000 0.0000 19.9800 + 35270.0000 0.2417 19.7473 + 35270.0000 0.4833 19.5147 + 35270.0000 0.7250 19.2820 + 35270.0000 0.9667 19.0493 + 35270.0000 1.2083 18.8167 + 35270.0000 1.4500 18.5840 + 35270.0000 1.6917 18.3513 + 35270.0000 1.9333 18.1187 + 35270.0000 2.1750 17.8860 + 35270.0000 2.4167 17.6533 + 35270.0000 2.6583 17.4207 + 35270.0000 2.9000 17.1880 rg + 35270.0000 3.8307 17.0550 + 35270.0000 4.7613 16.9221 + 35270.0000 5.6920 16.7891 + 35270.0000 6.6235 16.6629 + 35270.0000 7.5577 16.5578 + 35270.0000 8.4920 16.4527 + 35270.0000 9.4262 16.3476 + 35270.0000 10.3652 16.3280 + 35270.0000 11.3053 16.3280 + 35270.0000 12.2454 16.3280 + 35270.0000 13.0862 16.5862 + 35270.0000 13.7800 17.2200 rd + 35270.0000 13.9817 17.4267 + 35270.0000 14.1833 17.6333 + 35270.0000 14.3850 17.8400 + 35270.0000 14.5867 18.0467 + 35270.0000 14.7883 18.2533 + 35270.0000 14.9900 18.4600 + 35270.0000 15.1917 18.6667 + 35270.0000 15.3933 18.8733 + 35270.0000 15.5950 19.0800 + 35270.0000 15.7967 19.2867 + 35270.0000 15.9983 19.4933 + 35270.0000 16.2000 19.7000 + 999.9990 999.9990 0.0000 + 95 34 4 37 35315.0000 + 35315.0000 0.0000 19.9200 + 35315.0000 0.2417 19.6895 + 35315.0000 0.4833 19.4590 + 35315.0000 0.7250 19.2285 + 35315.0000 0.9667 18.9980 + 35315.0000 1.2083 18.7675 + 35315.0000 1.4500 18.5370 + 35315.0000 1.6917 18.3065 + 35315.0000 1.9333 18.0760 + 35315.0000 2.1750 17.8455 + 35315.0000 2.4167 17.6150 + 35315.0000 2.6583 17.3845 + 35315.0000 2.9000 17.1540 rg + 35315.0000 3.8273 17.0215 + 35315.0000 4.7547 16.8890 + 35315.0000 5.6820 16.7566 + 35315.0000 6.6101 16.6304 + 35315.0000 7.5410 16.5256 + 35315.0000 8.4719 16.4209 + 35315.0000 9.4028 16.3162 + 35315.0000 10.3383 16.2940 + 35315.0000 11.2750 16.2940 + 35315.0000 12.2118 16.2940 + 35315.0000 13.0557 16.5307 + 35315.0000 13.7400 17.1700 rd + 35315.0000 13.9450 17.3708 + 35315.0000 14.1500 17.5717 + 35315.0000 14.3550 17.7725 + 35315.0000 14.5600 17.9733 + 35315.0000 14.7650 18.1742 + 35315.0000 14.9700 18.3750 + 35315.0000 15.1750 18.5758 + 35315.0000 15.3800 18.7767 + 35315.0000 15.5850 18.9775 + 35315.0000 15.7900 19.1783 + 35315.0000 15.9950 19.3792 + 35315.0000 16.2000 19.5800 + 999.9990 999.9990 0.0000 + 96 35 0 37 35360.0000 + 35360.0000 0.0000 19.8600 + 35360.0000 0.2417 19.6317 + 35360.0000 0.4833 19.4033 + 35360.0000 0.7250 19.1750 + 35360.0000 0.9667 18.9467 + 35360.0000 1.2083 18.7183 + 35360.0000 1.4500 18.4900 + 35360.0000 1.6917 18.2617 + 35360.0000 1.9333 18.0333 + 35360.0000 2.1750 17.8050 + 35360.0000 2.4167 17.5767 + 35360.0000 2.6583 17.3483 + 35360.0000 2.9000 17.1200 rg + 35360.0000 3.8240 16.9880 + 35360.0000 4.7480 16.8560 + 35360.0000 5.6720 16.7240 + 35360.0000 6.5967 16.5979 + 35360.0000 7.5243 16.4935 + 35360.0000 8.4518 16.3892 + 35360.0000 9.3793 16.2848 + 35360.0000 10.3113 16.2600 + 35360.0000 11.2447 16.2600 + 35360.0000 12.1781 16.2600 + 35360.0000 13.0252 16.4752 + 35360.0000 13.7000 17.1200 rd + 35360.0000 13.9083 17.3150 + 35360.0000 14.1167 17.5100 + 35360.0000 14.3250 17.7050 + 35360.0000 14.5333 17.9000 + 35360.0000 14.7417 18.0950 + 35360.0000 14.9500 18.2900 + 35360.0000 15.1583 18.4850 + 35360.0000 15.3667 18.6800 + 35360.0000 15.5750 18.8750 + 35360.0000 15.7833 19.0700 + 35360.0000 15.9917 19.2650 + 35360.0000 16.2000 19.4600 + 999.9990 999.9990 0.0000 + 97 35 1 37 35415.0000 + 35415.0000 0.0000 19.8950 + 35415.0000 0.2417 19.6602 + 35415.0000 0.4833 19.4254 + 35415.0000 0.7250 19.1906 + 35415.0000 0.9667 18.9558 + 35415.0000 1.2083 18.7210 + 35415.0000 1.4500 18.4863 + 35415.0000 1.6917 18.2515 + 35415.0000 1.9333 18.0167 + 35415.0000 2.1750 17.7819 + 35415.0000 2.4167 17.5471 + 35415.0000 2.6583 17.3123 + 35415.0000 2.9000 17.0775 rg + 35415.0000 3.8023 16.7626 + 35415.0000 4.8961 16.6617 + 35415.0000 5.9899 16.5609 + 35415.0000 7.0843 16.4644 + 35415.0000 8.1807 16.3842 + 35415.0000 9.2772 16.3047 + 35415.0000 10.3736 16.2283 + 35415.0000 11.4734 16.2116 + 35415.0000 12.5742 16.2135 + 35415.0000 13.6751 16.2153 + 35415.0000 14.7112 16.3786 + 35415.0000 15.5250 17.0975 rd + 35415.0000 15.7229 17.2952 + 35415.0000 15.9208 17.4929 + 35415.0000 16.1187 17.6906 + 35415.0000 16.3167 17.8883 + 35415.0000 16.5146 18.0860 + 35415.0000 16.7125 18.2837 + 35415.0000 16.9104 18.4815 + 35415.0000 17.1083 18.6792 + 35415.0000 17.3062 18.8769 + 35415.0000 17.5042 19.0746 + 35415.0000 17.7021 19.2723 + 35415.0000 17.9000 19.4700 + 999.9990 999.9990 0.0000 + 98 35 2 37 35470.0000 + 35470.0000 0.0000 19.9300 + 35470.0000 0.2417 19.6887 + 35470.0000 0.4833 19.4475 + 35470.0000 0.7250 19.2063 + 35470.0000 0.9667 18.9650 + 35470.0000 1.2083 18.7238 + 35470.0000 1.4500 18.4825 + 35470.0000 1.6917 18.2413 + 35470.0000 1.9333 18.0000 + 35470.0000 2.1750 17.7587 + 35470.0000 2.4167 17.5175 + 35470.0000 2.6583 17.2762 + 35470.0000 2.9000 17.0350 rg + 35470.0000 3.7807 16.5373 + 35470.0000 5.0443 16.4675 + 35470.0000 6.3079 16.3977 + 35470.0000 7.5718 16.3309 + 35470.0000 8.8372 16.2749 + 35470.0000 10.1026 16.2202 + 35470.0000 11.3679 16.1718 + 35470.0000 12.6355 16.1631 + 35470.0000 13.9038 16.1669 + 35470.0000 15.1721 16.1707 + 35470.0000 16.3972 16.2820 + 35470.0000 17.3500 17.0750 rd + 35470.0000 17.5375 17.2754 + 35470.0000 17.7250 17.4758 + 35470.0000 17.9125 17.6763 + 35470.0000 18.1000 17.8767 + 35470.0000 18.2875 18.0771 + 35470.0000 18.4750 18.2775 + 35470.0000 18.6625 18.4779 + 35470.0000 18.8500 18.6783 + 35470.0000 19.0375 18.8788 + 35470.0000 19.2250 19.0792 + 35470.0000 19.4125 19.2796 + 35470.0000 19.6000 19.4800 + 999.9990 999.9990 0.0000 + 99 35 3 37 35525.0000 + 35525.0000 0.0000 19.9650 + 35525.0000 0.2417 19.7173 + 35525.0000 0.4833 19.4696 + 35525.0000 0.7250 19.2219 + 35525.0000 0.9667 18.9742 + 35525.0000 1.2083 18.7265 + 35525.0000 1.4500 18.4788 + 35525.0000 1.6917 18.2310 + 35525.0000 1.9333 17.9833 + 35525.0000 2.1750 17.7356 + 35525.0000 2.4167 17.4879 + 35525.0000 2.6583 17.2402 + 35525.0000 2.9000 16.9925 rg + 35525.0000 3.7590 16.3119 + 35525.0000 5.1924 16.2732 + 35525.0000 6.6258 16.2346 + 35525.0000 8.0594 16.1974 + 35525.0000 9.4937 16.1656 + 35525.0000 10.9279 16.1357 + 35525.0000 12.3622 16.1153 + 35525.0000 13.7976 16.1147 + 35525.0000 15.2333 16.1204 + 35525.0000 16.6691 16.1260 + 35525.0000 18.0832 16.1855 + 35525.0000 19.1750 17.0525 rd + 35525.0000 19.3521 17.2556 + 35525.0000 19.5292 17.4588 + 35525.0000 19.7063 17.6619 + 35525.0000 19.8833 17.8650 + 35525.0000 20.0604 18.0681 + 35525.0000 20.2375 18.2713 + 35525.0000 20.4146 18.4744 + 35525.0000 20.5917 18.6775 + 35525.0000 20.7688 18.8806 + 35525.0000 20.9458 19.0838 + 35525.0000 21.1229 19.2869 + 35525.0000 21.3000 19.4900 + 999.9990 999.9990 0.0000 + 100 36 0 37 35580.0000 + 35580.0000 0.0000 20.0000 + 35580.0000 0.2417 19.7458 + 35580.0000 0.4833 19.4917 + 35580.0000 0.7250 19.2375 + 35580.0000 0.9667 18.9833 + 35580.0000 1.2083 18.7292 + 35580.0000 1.4500 18.4750 + 35580.0000 1.6917 18.2208 + 35580.0000 1.9333 17.9667 + 35580.0000 2.1750 17.7125 + 35580.0000 2.4167 17.4583 + 35580.0000 2.6583 17.2042 + 35580.0000 2.9000 16.9500 rg + 35580.0000 3.7374 16.0865 + 35580.0000 5.3406 16.0790 + 35580.0000 6.9438 16.0714 + 35580.0000 8.5470 16.0639 + 35580.0000 10.1501 16.0564 + 35580.0000 11.7533 16.0512 + 35580.0000 13.3565 16.0587 + 35580.0000 14.9597 16.0663 + 35580.0000 16.5629 16.0738 + 35580.0000 18.1661 16.0814 + 35580.0000 19.7692 16.0889 + 35580.0000 21.0000 17.0300 rd + 35580.0000 21.1667 17.2358 + 35580.0000 21.3333 17.4417 + 35580.0000 21.5000 17.6475 + 35580.0000 21.6667 17.8533 + 35580.0000 21.8333 18.0592 + 35580.0000 22.0000 18.2650 + 35580.0000 22.1667 18.4708 + 35580.0000 22.3333 18.6767 + 35580.0000 22.5000 18.8825 + 35580.0000 22.6667 19.0883 + 35580.0000 22.8333 19.2942 + 35580.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 101 36 1 37 35628.5714 + 35628.5714 0.0000 20.0000 + 35628.5714 0.2333 19.7348 + 35628.5714 0.4667 19.4695 + 35628.5714 0.7000 19.2043 + 35628.5714 0.9333 18.9390 + 35628.5714 1.1667 18.6738 + 35628.5714 1.4000 18.4086 + 35628.5714 1.6333 18.1433 + 35628.5714 1.8667 17.8781 + 35628.5714 2.1000 17.6129 + 35628.5714 2.3333 17.3476 + 35628.5714 2.5667 17.0824 + 35628.5714 2.8000 16.8171 rg + 35628.5714 3.6358 15.8670 + 35628.5714 5.2528 15.8591 + 35628.5714 6.8698 15.8512 + 35628.5714 8.4867 15.8433 + 35628.5714 10.1037 15.8354 + 35628.5714 11.7207 15.8296 + 35628.5714 13.3377 15.8375 + 35628.5714 14.9547 15.8454 + 35628.5714 16.5717 15.8533 + 35628.5714 18.1886 15.8612 + 35628.5714 19.8040 15.8720 + 35628.5714 20.9857 16.8857 rd + 35628.5714 21.1536 17.1036 + 35628.5714 21.3214 17.3214 + 35628.5714 21.4893 17.5393 + 35628.5714 21.6571 17.7571 + 35628.5714 21.8250 17.9750 + 35628.5714 21.9929 18.1929 + 35628.5714 22.1607 18.4107 + 35628.5714 22.3286 18.6286 + 35628.5714 22.4964 18.8464 + 35628.5714 22.6643 19.0643 + 35628.5714 22.8321 19.2821 + 35628.5714 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 102 36 2 37 35677.1429 + 35677.1429 0.0000 20.0000 + 35677.1429 0.2250 19.7237 + 35677.1429 0.4500 19.4474 + 35677.1429 0.6750 19.1711 + 35677.1429 0.9000 18.8948 + 35677.1429 1.1250 18.6185 + 35677.1429 1.3500 18.3421 + 35677.1429 1.5750 18.0658 + 35677.1429 1.8000 17.7895 + 35677.1429 2.0250 17.5132 + 35677.1429 2.2500 17.2369 + 35677.1429 2.4750 16.9606 + 35677.1429 2.7000 16.6843 rg + 35677.1429 3.5342 15.6475 + 35677.1429 5.1650 15.6392 + 35677.1429 6.7958 15.6310 + 35677.1429 8.4265 15.6227 + 35677.1429 10.0573 15.6145 + 35677.1429 11.6881 15.6080 + 35677.1429 13.3189 15.6163 + 35677.1429 14.9496 15.6245 + 35677.1429 16.5804 15.6328 + 35677.1429 18.2112 15.6410 + 35677.1429 19.8387 15.6550 + 35677.1429 20.9714 16.7414 rd + 35677.1429 21.1405 16.9713 + 35677.1429 21.3095 17.2012 + 35677.1429 21.4786 17.4311 + 35677.1429 21.6476 17.6610 + 35677.1429 21.8167 17.8908 + 35677.1429 21.9857 18.1207 + 35677.1429 22.1548 18.3506 + 35677.1429 22.3238 18.5805 + 35677.1429 22.4929 18.8104 + 35677.1429 22.6619 19.0402 + 35677.1429 22.8310 19.2701 + 35677.1429 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 103 36 3 37 35725.7143 + 35725.7143 0.0000 20.0000 + 35725.7143 0.2167 19.7126 + 35725.7143 0.4333 19.4252 + 35725.7143 0.6500 19.1379 + 35725.7143 0.8667 18.8505 + 35725.7143 1.0833 18.5631 + 35725.7143 1.3000 18.2757 + 35725.7143 1.5167 17.9883 + 35725.7143 1.7333 17.7010 + 35725.7143 1.9500 17.4136 + 35725.7143 2.1667 17.1262 + 35725.7143 2.3833 16.8388 + 35725.7143 2.6000 16.5514 rg + 35725.7143 3.4326 15.4280 + 35725.7143 5.0772 15.4194 + 35725.7143 6.7218 15.4108 + 35725.7143 8.3663 15.4022 + 35725.7143 10.0109 15.3936 + 35725.7143 11.6555 15.3865 + 35725.7143 13.3001 15.3951 + 35725.7143 14.9446 15.4037 + 35725.7143 16.5892 15.4122 + 35725.7143 18.2338 15.4208 + 35725.7143 19.8735 15.4381 + 35725.7143 20.9571 16.5971 rd + 35725.7143 21.1274 16.8390 + 35725.7143 21.2976 17.0810 + 35725.7143 21.4679 17.3229 + 35725.7143 21.6381 17.5648 + 35725.7143 21.8083 17.8067 + 35725.7143 21.9786 18.0486 + 35725.7143 22.1488 18.2905 + 35725.7143 22.3190 18.5324 + 35725.7143 22.4893 18.7743 + 35725.7143 22.6595 19.0162 + 35725.7143 22.8298 19.2581 + 35725.7143 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 104 36 4 37 35774.2857 + 35774.2857 0.0000 20.0000 + 35774.2857 0.2083 19.7015 + 35774.2857 0.4167 19.4031 + 35774.2857 0.6250 19.1046 + 35774.2857 0.8333 18.8062 + 35774.2857 1.0417 18.5077 + 35774.2857 1.2500 18.2093 + 35774.2857 1.4583 17.9108 + 35774.2857 1.6667 17.6124 + 35774.2857 1.8750 17.3139 + 35774.2857 2.0833 17.0155 + 35774.2857 2.2917 16.7170 + 35774.2857 2.5000 16.4186 rg + 35774.2857 3.3310 15.2084 + 35774.2857 4.9894 15.1995 + 35774.2857 6.6477 15.1905 + 35774.2857 8.3061 15.1816 + 35774.2857 9.9645 15.1726 + 35774.2857 11.6229 15.1649 + 35774.2857 13.2812 15.1738 + 35774.2857 14.9396 15.1828 + 35774.2857 16.5980 15.1917 + 35774.2857 18.2564 15.2007 + 35774.2857 19.9082 15.2212 + 35774.2857 20.9429 16.4529 rd + 35774.2857 21.1143 16.7068 + 35774.2857 21.2857 16.9607 + 35774.2857 21.4571 17.2146 + 35774.2857 21.6286 17.4686 + 35774.2857 21.8000 17.7225 + 35774.2857 21.9714 17.9764 + 35774.2857 22.1429 18.2304 + 35774.2857 22.3143 18.4843 + 35774.2857 22.4857 18.7382 + 35774.2857 22.6571 18.9921 + 35774.2857 22.8286 19.2461 + 35774.2857 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 105 36 5 37 35822.8571 + 35822.8571 0.0000 20.0000 + 35822.8571 0.2000 19.6905 + 35822.8571 0.4000 19.3810 + 35822.8571 0.6000 19.0714 + 35822.8571 0.8000 18.7619 + 35822.8571 1.0000 18.4524 + 35822.8571 1.2000 18.1429 + 35822.8571 1.4000 17.8333 + 35822.8571 1.6000 17.5238 + 35822.8571 1.8000 17.2143 + 35822.8571 2.0000 16.9048 + 35822.8571 2.2000 16.5952 + 35822.8571 2.4000 16.2857 rg + 35822.8571 3.2294 14.9889 + 35822.8571 4.9016 14.9796 + 35822.8571 6.5737 14.9703 + 35822.8571 8.2459 14.9610 + 35822.8571 9.9181 14.9517 + 35822.8571 11.5902 14.9433 + 35822.8571 13.2624 14.9526 + 35822.8571 14.9346 14.9619 + 35822.8571 16.6068 14.9712 + 35822.8571 18.2789 14.9805 + 35822.8571 19.9430 15.0042 + 35822.8571 20.9286 16.3086 rd + 35822.8571 21.1012 16.5745 + 35822.8571 21.2738 16.8405 + 35822.8571 21.4464 17.1064 + 35822.8571 21.6190 17.3724 + 35822.8571 21.7917 17.6383 + 35822.8571 21.9643 17.9043 + 35822.8571 22.1369 18.1702 + 35822.8571 22.3095 18.4362 + 35822.8571 22.4821 18.7021 + 35822.8571 22.6548 18.9681 + 35822.8571 22.8274 19.2340 + 35822.8571 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 106 36 6 37 35871.4286 + 35871.4286 0.0000 20.0000 + 35871.4286 0.1917 19.6794 + 35871.4286 0.3833 19.3588 + 35871.4286 0.5750 19.0382 + 35871.4286 0.7667 18.7176 + 35871.4286 0.9583 18.3970 + 35871.4286 1.1500 18.0764 + 35871.4286 1.3417 17.7558 + 35871.4286 1.5333 17.4352 + 35871.4286 1.7250 17.1146 + 35871.4286 1.9167 16.7940 + 35871.4286 2.1083 16.4735 + 35871.4286 2.3000 16.1529 rg + 35871.4286 3.1278 14.7694 + 35871.4286 4.8138 14.7597 + 35871.4286 6.4997 14.7501 + 35871.4286 8.1857 14.7404 + 35871.4286 9.8717 14.7308 + 35871.4286 11.5576 14.7217 + 35871.4286 13.2436 14.7314 + 35871.4286 14.9296 14.7410 + 35871.4286 16.6155 14.7507 + 35871.4286 18.3015 14.7603 + 35871.4286 19.9777 14.7873 + 35871.4286 20.9143 16.1643 rd + 35871.4286 21.0881 16.4423 + 35871.4286 21.2619 16.7202 + 35871.4286 21.4357 16.9982 + 35871.4286 21.6095 17.2762 + 35871.4286 21.7833 17.5542 + 35871.4286 21.9571 17.8321 + 35871.4286 22.1310 18.1101 + 35871.4286 22.3048 18.3881 + 35871.4286 22.4786 18.6661 + 35871.4286 22.6524 18.9440 + 35871.4286 22.8262 19.2220 + 35871.4286 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 107 37 0 37 35920.0000 + 35920.0000 0.0000 20.0000 + 35920.0000 0.1833 19.6683 + 35920.0000 0.3667 19.3367 + 35920.0000 0.5500 19.0050 + 35920.0000 0.7333 18.6733 + 35920.0000 0.9167 18.3417 + 35920.0000 1.1000 18.0100 + 35920.0000 1.2833 17.6783 + 35920.0000 1.4667 17.3467 + 35920.0000 1.6500 17.0150 + 35920.0000 1.8333 16.6833 + 35920.0000 2.0167 16.3517 + 35920.0000 2.2000 16.0200 rg + 35920.0000 3.0262 14.5498 + 35920.0000 4.7260 14.5398 + 35920.0000 6.4257 14.5298 + 35920.0000 8.1255 14.5199 + 35920.0000 9.8253 14.5099 + 35920.0000 11.5250 14.5001 + 35920.0000 13.2248 14.5101 + 35920.0000 14.9245 14.5201 + 35920.0000 16.6243 14.5301 + 35920.0000 18.3241 14.5401 + 35920.0000 20.0124 14.5703 + 35920.0000 20.9000 16.0200 rd + 35920.0000 21.0750 16.3100 + 35920.0000 21.2500 16.6000 + 35920.0000 21.4250 16.8900 + 35920.0000 21.6000 17.1800 + 35920.0000 21.7750 17.4700 + 35920.0000 21.9500 17.7600 + 35920.0000 22.1250 18.0500 + 35920.0000 22.3000 18.3400 + 35920.0000 22.4750 18.6300 + 35920.0000 22.6500 18.9200 + 35920.0000 22.8250 19.2100 + 35920.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 108 37 1 37 35970.0000 S1 + 35970.0000 0.0000 20.0000 + 35970.0000 0.1786 19.6801 + 35970.0000 0.3571 19.3602 + 35970.0000 0.5357 19.0404 + 35970.0000 0.7143 18.7205 + 35970.0000 0.8929 18.4006 + 35970.0000 1.0714 18.0807 + 35970.0000 1.2500 17.7608 + 35970.0000 1.4286 17.4410 + 35970.0000 1.6071 17.1211 + 35970.0000 1.7857 16.8012 + 35970.0000 1.9643 16.4813 + 35970.0000 2.1429 16.1614 rg + 35970.0000 2.9868 14.6829 + 35970.0000 4.6690 14.6163 + 35970.0000 6.3831 14.6069 + 35970.0000 8.0972 14.5974 + 35970.0000 9.8112 14.5879 + 35970.0000 11.5253 14.5787 + 35970.0000 13.2394 14.5882 + 35970.0000 14.9535 14.5977 + 35970.0000 16.6676 14.6071 + 35970.0000 18.3816 14.6166 + 35970.0000 20.0527 14.7056 + 35970.0000 20.9571 16.1614 rd + 35970.0000 21.1274 16.4396 + 35970.0000 21.2976 16.7179 + 35970.0000 21.4679 16.9961 + 35970.0000 21.6381 17.2743 + 35970.0000 21.8083 17.5525 + 35970.0000 21.9786 17.8307 + 35970.0000 22.1488 18.1089 + 35970.0000 22.3190 18.3871 + 35970.0000 22.4893 18.6654 + 35970.0000 22.6595 18.9436 + 35970.0000 22.8298 19.2218 + 35970.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 109 37 2 37 36020.0000 S1 + 36020.0000 0.0000 20.0000 + 36020.0000 0.1738 19.6919 + 36020.0000 0.3476 19.3838 + 36020.0000 0.5214 19.0757 + 36020.0000 0.6952 18.7676 + 36020.0000 0.8690 18.4595 + 36020.0000 1.0429 18.1514 + 36020.0000 1.2167 17.8433 + 36020.0000 1.3905 17.5352 + 36020.0000 1.5643 17.2271 + 36020.0000 1.7381 16.9190 + 36020.0000 1.9119 16.6110 + 36020.0000 2.0857 16.3029 rg + 36020.0000 2.9474 14.8160 + 36020.0000 4.6121 14.6928 + 36020.0000 6.3405 14.6839 + 36020.0000 8.0688 14.6749 + 36020.0000 9.7972 14.6660 + 36020.0000 11.5256 14.6573 + 36020.0000 13.2540 14.6662 + 36020.0000 14.9824 14.6752 + 36020.0000 16.7108 14.6841 + 36020.0000 18.4392 14.6931 + 36020.0000 20.0930 14.8408 + 36020.0000 21.0143 16.3029 rd + 36020.0000 21.1798 16.5693 + 36020.0000 21.3452 16.8357 + 36020.0000 21.5107 17.1021 + 36020.0000 21.6762 17.3686 + 36020.0000 21.8417 17.6350 + 36020.0000 22.0071 17.9014 + 36020.0000 22.1726 18.1679 + 36020.0000 22.3381 18.4343 + 36020.0000 22.5036 18.7007 + 36020.0000 22.6690 18.9671 + 36020.0000 22.8345 19.2336 + 36020.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 110 37 3 37 36070.0000 S1 + 36070.0000 0.0000 20.0000 + 36070.0000 0.1690 19.7037 + 36070.0000 0.3381 19.4074 + 36070.0000 0.5071 19.1111 + 36070.0000 0.6762 18.8148 + 36070.0000 0.8452 18.5185 + 36070.0000 1.0143 18.2221 + 36070.0000 1.1833 17.9258 + 36070.0000 1.3524 17.6295 + 36070.0000 1.5214 17.3332 + 36070.0000 1.6905 17.0369 + 36070.0000 1.8595 16.7406 + 36070.0000 2.0286 16.4443 rg + 36070.0000 2.9080 14.9491 + 36070.0000 4.5551 14.7693 + 36070.0000 6.2978 14.7609 + 36070.0000 8.0405 14.7525 + 36070.0000 9.7832 14.7440 + 36070.0000 11.5259 14.7358 + 36070.0000 13.2687 14.7443 + 36070.0000 15.0114 14.7527 + 36070.0000 16.7541 14.7611 + 36070.0000 18.4968 14.7696 + 36070.0000 20.1333 14.9761 + 36070.0000 21.0714 16.4443 rd + 36070.0000 21.2321 16.6989 + 36070.0000 21.3929 16.9536 + 36070.0000 21.5536 17.2082 + 36070.0000 21.7143 17.4629 + 36070.0000 21.8750 17.7175 + 36070.0000 22.0357 17.9721 + 36070.0000 22.1964 18.2268 + 36070.0000 22.3571 18.4814 + 36070.0000 22.5179 18.7361 + 36070.0000 22.6786 18.9907 + 36070.0000 22.8393 19.2454 + 36070.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 111 37 4 37 36120.0000 S1 + 36120.0000 0.0000 20.0000 + 36120.0000 0.1643 19.7155 + 36120.0000 0.3286 19.4310 + 36120.0000 0.4929 19.1464 + 36120.0000 0.6571 18.8619 + 36120.0000 0.8214 18.5774 + 36120.0000 0.9857 18.2929 + 36120.0000 1.1500 18.0083 + 36120.0000 1.3143 17.7238 + 36120.0000 1.4786 17.4393 + 36120.0000 1.6429 17.1548 + 36120.0000 1.8071 16.8702 + 36120.0000 1.9714 16.5857 rg + 36120.0000 2.8686 15.0822 + 36120.0000 4.4982 14.8458 + 36120.0000 6.2552 14.8379 + 36120.0000 8.0122 14.8300 + 36120.0000 9.7692 14.8221 + 36120.0000 11.5263 14.8144 + 36120.0000 13.2833 14.8223 + 36120.0000 15.0403 14.8302 + 36120.0000 16.7973 14.8381 + 36120.0000 18.5543 14.8461 + 36120.0000 20.1736 15.1113 + 36120.0000 21.1286 16.5857 rd + 36120.0000 21.2845 16.8286 + 36120.0000 21.4405 17.0714 + 36120.0000 21.5964 17.3143 + 36120.0000 21.7524 17.5571 + 36120.0000 21.9083 17.8000 + 36120.0000 22.0643 18.0429 + 36120.0000 22.2202 18.2857 + 36120.0000 22.3762 18.5286 + 36120.0000 22.5321 18.7714 + 36120.0000 22.6881 19.0143 + 36120.0000 22.8440 19.2571 + 36120.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 112 37 5 37 36170.0000 S1 + 36170.0000 0.0000 20.0000 + 36170.0000 0.1595 19.7273 + 36170.0000 0.3190 19.4545 + 36170.0000 0.4786 19.1818 + 36170.0000 0.6381 18.9090 + 36170.0000 0.7976 18.6363 + 36170.0000 0.9571 18.3636 + 36170.0000 1.1167 18.0908 + 36170.0000 1.2762 17.8181 + 36170.0000 1.4357 17.5454 + 36170.0000 1.5952 17.2726 + 36170.0000 1.7548 16.9999 + 36170.0000 1.9143 16.7271 rg + 36170.0000 2.8292 15.2152 + 36170.0000 4.4412 14.9223 + 36170.0000 6.2125 14.9149 + 36170.0000 7.9839 14.9075 + 36170.0000 9.7552 14.9001 + 36170.0000 11.5266 14.8930 + 36170.0000 13.2979 14.9004 + 36170.0000 15.0692 14.9078 + 36170.0000 16.8406 14.9152 + 36170.0000 18.6119 14.9225 + 36170.0000 20.2139 15.2466 + 36170.0000 21.1857 16.7271 rd + 36170.0000 21.3369 16.9582 + 36170.0000 21.4881 17.1893 + 36170.0000 21.6393 17.4204 + 36170.0000 21.7905 17.6514 + 36170.0000 21.9417 17.8825 + 36170.0000 22.0929 18.1136 + 36170.0000 22.2440 18.3446 + 36170.0000 22.3952 18.5757 + 36170.0000 22.5464 18.8068 + 36170.0000 22.6976 19.0379 + 36170.0000 22.8488 19.2689 + 36170.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 113 37 6 37 36220.0000 S1 + 36220.0000 0.0000 20.0000 + 36220.0000 0.1548 19.7390 + 36220.0000 0.3095 19.4781 + 36220.0000 0.4643 19.2171 + 36220.0000 0.6190 18.9562 + 36220.0000 0.7738 18.6952 + 36220.0000 0.9286 18.4343 + 36220.0000 1.0833 18.1733 + 36220.0000 1.2381 17.9124 + 36220.0000 1.3929 17.6514 + 36220.0000 1.5476 17.3905 + 36220.0000 1.7024 17.1295 + 36220.0000 1.8571 16.8686 rg + 36220.0000 2.7898 15.3483 + 36220.0000 4.3843 14.9988 + 36220.0000 6.1699 14.9919 + 36220.0000 7.9556 14.9851 + 36220.0000 9.7412 14.9782 + 36220.0000 11.5269 14.9715 + 36220.0000 13.3125 14.9784 + 36220.0000 15.0982 14.9853 + 36220.0000 16.8838 14.9922 + 36220.0000 18.6695 14.9990 + 36220.0000 20.2541 15.3819 + 36220.0000 21.2429 16.8686 rd + 36220.0000 21.3893 17.0879 + 36220.0000 21.5357 17.3071 + 36220.0000 21.6821 17.5264 + 36220.0000 21.8286 17.7457 + 36220.0000 21.9750 17.9650 + 36220.0000 22.1214 18.1843 + 36220.0000 22.2679 18.4036 + 36220.0000 22.4143 18.6229 + 36220.0000 22.5607 18.8421 + 36220.0000 22.7071 19.0614 + 36220.0000 22.8536 19.2807 + 36220.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 114 38 0 37 36270.0000 S1 + 36270.0000 0.0000 20.0000 + 36270.0000 0.1500 19.7508 + 36270.0000 0.3000 19.5017 + 36270.0000 0.4500 19.2525 + 36270.0000 0.6000 19.0033 + 36270.0000 0.7500 18.7542 + 36270.0000 0.9000 18.5050 + 36270.0000 1.0500 18.2558 + 36270.0000 1.2000 18.0067 + 36270.0000 1.3500 17.7575 + 36270.0000 1.5000 17.5083 + 36270.0000 1.6500 17.2592 + 36270.0000 1.8000 17.0100 rg + 36270.0000 2.7504 15.4814 + 36270.0000 4.3273 15.0753 + 36270.0000 6.1273 15.0690 + 36270.0000 7.9272 15.0626 + 36270.0000 9.7272 15.0563 + 36270.0000 11.5272 15.0501 + 36270.0000 13.3271 15.0564 + 36270.0000 15.1271 15.0628 + 36270.0000 16.9271 15.0692 + 36270.0000 18.7270 15.0755 + 36270.0000 20.2944 15.5171 + 36270.0000 21.3000 17.0100 rd + 36270.0000 21.4417 17.2175 + 36270.0000 21.5833 17.4250 + 36270.0000 21.7250 17.6325 + 36270.0000 21.8667 17.8400 + 36270.0000 22.0083 18.0475 + 36270.0000 22.1500 18.2550 + 36270.0000 22.2917 18.4625 + 36270.0000 22.4333 18.6700 + 36270.0000 22.5750 18.8775 + 36270.0000 22.7167 19.0850 + 36270.0000 22.8583 19.2925 + 36270.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 + 115 39 0 37 36271.0000 S1bis + 36271.0000 0.0000 20.0000 + 36271.0000 0.1500 19.7508 + 36271.0000 0.3000 19.5017 + 36271.0000 0.4500 19.2525 + 36271.0000 0.6000 19.0033 + 36271.0000 0.7500 18.7542 + 36271.0000 0.9000 18.5050 + 36271.0000 1.0500 18.2558 + 36271.0000 1.2000 18.0067 + 36271.0000 1.3500 17.7575 + 36271.0000 1.5000 17.5083 + 36271.0000 1.6500 17.2592 + 36271.0000 1.8000 17.0100 rg + 36271.0000 2.4145 14.9977 + 36271.0000 3.0990 13.0797 + 36271.0000 5.2030 13.0722 + 36271.0000 7.3071 13.0648 + 36271.0000 9.4111 13.0574 + 36271.0000 11.5152 13.0501 + 36271.0000 13.6192 13.0575 + 36271.0000 15.7232 13.0649 + 36271.0000 17.8273 13.0723 + 36271.0000 19.9313 13.0798 + 36271.0000 20.6392 15.0124 + 36271.0000 21.3000 17.0100 rd + 36271.0000 21.4417 17.2175 + 36271.0000 21.5833 17.4250 + 36271.0000 21.7250 17.6325 + 36271.0000 21.8667 17.8400 + 36271.0000 22.0083 18.0475 + 36271.0000 22.1500 18.2550 + 36271.0000 22.2917 18.4625 + 36271.0000 22.4333 18.6700 + 36271.0000 22.5750 18.8775 + 36271.0000 22.7167 19.0850 + 36271.0000 22.8583 19.2925 + 36271.0000 23.0000 19.5000 + 999.9990 999.9990 0.0000 diff --git a/doc/users/Tuto1/data/Bief_1.ST b/doc/users/Tuto1/data/Bief_1.ST new file mode 100644 index 00000000..291d40f2 --- /dev/null +++ b/doc/users/Tuto1/data/Bief_1.ST @@ -0,0 +1,442 @@ + 1 0 0 8 31090.0000 + 31090.0000 0.0000 23.7300 + 31090.0000 2.5000 20.2800 rg + 31090.0000 2.5000 19.2900 + 31090.0000 4.4000 19.2900 + 31090.0000 10.7000 19.5900 + 31090.0000 10.7000 20.5900 rd + 31090.0000 14.5000 21.7100 + 31090.0000 17.6000 23.7300 + 999.9990 999.9990 999.9990 + 2 0 0 8 31165.0000 + 31165.0000 0.0000 22.8400 + 31165.0000 2.5000 20.1300 rg + 31165.0000 2.5000 19.2300 + 31165.0000 4.4000 19.2300 + 31165.0000 10.7000 19.5300 + 31165.0000 10.7000 20.5300 rd + 31165.0000 14.5000 21.7500 + 31165.0000 17.6000 23.5000 + 999.9990 999.9990 999.9990 + 3 0 0 10 31425.0000 + 31425.0000 -1.0000 22.8000 + 31425.0000 0.0000 21.9000 + 31425.0000 1.7000 20.3600 rg + 31425.0000 2.5000 19.6300 + 31425.0000 2.5000 19.0300 + 31425.0000 4.4000 19.0300 + 31425.0000 10.7000 19.3300 + 31425.0000 10.7000 20.3300 rd + 31425.0000 14.5000 21.9200 + 31425.0000 17.6000 23.4200 + 999.9990 999.9990 999.9990 + 4 0 0 10 31590.0000 + 31590.0000 -1.0000 22.7500 + 31590.0000 0.0000 22.0500 + 31590.0000 1.6700 20.2200 rg + 31590.0000 2.5000 19.3100 + 31590.0000 2.5000 18.9000 + 31590.0000 4.4000 18.9000 + 31590.0000 10.7000 19.2000 + 31590.0000 10.7000 20.2000 rd + 31590.0000 14.5000 22.0200 + 31590.0000 17.6000 23.2500 + 999.9990 999.9990 999.9990 + 5 0 0 10 31780.0000 + 31780.0000 -1.0000 22.7000 + 31780.0000 0.0000 22.0500 + 31780.0000 1.6000 20.0500 rg + 31780.0000 2.5000 18.9400 + 31780.0000 2.5000 18.7500 + 31780.0000 4.4000 18.7500 + 31780.0000 10.7000 19.0500 + 31780.0000 10.7000 20.0500 rd + 31780.0000 14.5000 22.1400 + 31780.0000 17.6000 23.0000 + 999.9990 999.9990 999.9990 + 6 0 0 9 31874.0000 + 31874.0000 0.0000 22.9900 + 31874.0000 0.0000 22.6900 + 31874.0000 2.5000 19.6900 rg + 31874.0000 2.5000 18.6900 + 31874.0000 4.4000 18.6900 + 31874.0000 10.7000 18.9900 + 31874.0000 10.7000 19.9900 rd + 31874.0000 14.5000 22.1900 + 31874.0000 17.6000 22.9900 + 999.9990 999.9990 999.9990 + 7 0 0 9 31875.0000 PontRD101m + 31875.0000 0.5400 23.0000 + 31875.0000 0.5400 21.8900 + 31875.0000 2.5000 19.6900 rg + 31875.0000 2.5000 18.6900 + 31875.0000 4.4000 18.6900 + 31875.0000 10.7000 18.9900 + 31875.0000 10.7000 19.9900 rd + 31875.0000 13.8900 21.8900 + 31875.0000 13.8900 23.0000 + 999.9990 999.9990 999.9990 + 8 0 0 9 31885.0000 PontRD101v + 31885.0000 0.5400 23.0000 + 31885.0000 0.5400 21.8900 + 31885.0000 2.5000 19.6900 rg + 31885.0000 2.5000 18.6900 + 31885.0000 4.4000 18.6900 + 31885.0000 10.7000 18.9900 + 31885.0000 10.7000 19.9900 rd + 31885.0000 13.8900 21.8900 + 31885.0000 13.8900 23.0000 + 999.9990 999.9990 999.9990 + 9 0 0 9 31886.0000 + 31886.0000 0.0000 22.9900 + 31886.0000 0.0000 22.6900 + 31886.0000 2.5000 19.6900 rg + 31886.0000 2.5000 18.6900 + 31886.0000 4.4000 18.6900 + 31886.0000 10.7000 18.9900 + 31886.0000 10.7000 19.9900 rd + 31886.0000 14.5000 22.1900 + 31886.0000 17.6000 22.9900 + 999.9990 999.9990 999.9990 + 10 0 0 13 32015.0000 + 32015.0000 0.0000 22.6400 + 32015.0000 0.2000 22.2200 + 32015.0000 1.3000 20.9900 + 32015.0000 2.0000 19.8800 rg + 32015.0000 3.0000 18.6600 + 32015.0000 3.0000 18.5800 + 32015.0000 5.5000 18.5800 + 32015.0000 10.0000 18.9100 + 32015.0000 11.5000 19.2500 + 32015.0000 11.5000 20.0800 rd + 32015.0000 15.1000 22.0600 + 32015.0000 17.6000 22.5100 + 32015.0000 18.0000 22.8100 + 999.9990 999.9990 999.9990 + 11 0 0 13 32205.0000 + 32205.0000 0.0000 22.3300 + 32205.0000 0.2000 22.1800 + 32205.0000 1.5000 21.0600 + 32205.0000 2.5000 19.7700 rg + 32205.0000 3.6000 18.5100 + 32205.0000 3.6000 18.4500 + 32205.0000 6.9000 18.4500 + 32205.0000 8.8000 19.0500 + 32205.0000 12.5000 19.5700 rd + 32205.0000 12.5000 20.1800 + 32205.0000 15.8000 21.9000 + 32205.0000 18.1000 22.4000 + 32205.0000 18.6000 22.5500 + 999.9990 999.9990 999.9990 + 12 0 0 13 32460.0000 + 32460.0000 0.0000 22.1500 + 32460.0000 0.3000 22.1300 + 32460.0000 1.8000 21.1600 + 32460.0000 3.0000 19.6400 rg + 32460.0000 4.4000 18.3100 + 32460.0000 4.4000 18.2800 + 32460.0000 8.7000 18.2800 + 32460.0000 10.4000 19.2400 rd + 32460.0000 13.7000 19.9900 + 32460.0000 13.7000 20.3300 + 32460.0000 16.8000 21.6800 + 32460.0000 18.9000 22.2500 + 32460.0000 19.3000 22.5500 + 999.9990 999.9990 999.9990 + 13 0 0 13 32655.0000 + 32655.0000 0.0000 22.4500 + 32655.0000 0.3000 22.0900 + 32655.0000 2.1000 21.2300 + 32655.0000 3.4000 19.5300 rg + 32655.0000 5.0000 18.1600 + 32655.0000 5.0000 18.1400 + 32655.0000 10.1000 18.1400 + 32655.0000 11.6000 19.3900 rd + 32655.0000 14.7000 20.3100 + 32655.0000 14.7000 20.4300 + 32655.0000 17.5000 21.5200 + 32655.0000 19.4000 22.1400 + 32655.0000 19.8000 22.4500 + 999.9990 999.9990 999.9990 + 14 0 0 13 32755.0000 + 32755.0000 0.0000 22.4500 + 32755.0000 0.3000 22.0900 + 32755.0000 2.1000 21.2300 + 32755.0000 3.4000 19.5300 rg + 32755.0000 4.0000 18.0500 + 32755.0000 5.0000 18.0100 + 32755.0000 10.1000 18.0100 + 32755.0000 13.0000 19.3900 rd + 32755.0000 14.7000 20.3100 + 32755.0000 14.7000 20.4300 + 32755.0000 17.5000 21.5200 + 32755.0000 19.4000 22.1400 + 32755.0000 19.8000 22.4500 + 999.9990 999.9990 999.9990 + 15 0 0 10 32770.0000 PontA2 + 32770.0000 0.0000 25.0000 + 32770.0000 1.9000 25.0000 + 32770.0000 1.9000 21.2700 + 32770.0000 3.3000 19.4700 rg + 32770.0000 5.0000 18.0100 + 32770.0000 10.5000 18.0100 + 32770.0000 12.0000 19.4700 rd + 32770.0000 19.4000 21.4700 + 32770.0000 19.4000 25.0000 + 32770.0000 20.3000 25.0000 + 999.9990 999.9990 999.9990 + 16 0 0 10 32800.0000 PontA2 + 32800.0000 0.0000 25.0000 + 32800.0000 1.9000 25.0000 + 32800.0000 1.9000 21.2700 + 32800.0000 3.3000 19.4700 rg + 32800.0000 5.0000 18.0100 + 32800.0000 10.5000 18.0100 + 32800.0000 12.0000 19.4700 rd + 32800.0000 19.4000 21.4700 + 32800.0000 19.4000 25.0000 + 32800.0000 20.3000 25.0000 + 999.9990 999.9990 999.9990 + 17 0 0 13 32820.0000 + 32820.0000 0.0000 22.4500 + 32820.0000 0.3000 22.0900 + 32820.0000 2.1000 21.2300 + 32820.0000 3.4000 19.5300 rg + 32820.0000 4.0000 18.0500 + 32820.0000 5.0000 18.0100 + 32820.0000 10.1000 18.0100 + 32820.0000 13.0000 19.3900 rd + 32820.0000 14.7000 20.3100 + 32820.0000 14.7000 20.4300 + 32820.0000 17.5000 21.5200 + 32820.0000 19.4000 22.1400 + 32820.0000 19.8000 22.4500 + 999.9990 999.9990 999.9990 + 18 0 0 11 32915.0000 + 32915.0000 0.0000 22.5500 + 32915.0000 0.4000 21.9500 + 32915.0000 1.7000 21.2200 + 32915.0000 2.4000 20.9700 + 32915.0000 3.8000 19.4100 rg + 32915.0000 3.9000 19.3100 + 32915.0000 5.8000 18.0100 + 32915.0000 11.0000 18.0100 + 32915.0000 12.4000 19.3400 rd + 32915.0000 19.4000 21.9900 + 32915.0000 19.7000 22.5500 + 999.9990 999.9990 999.9990 + 19 0 0 10 33140.0000 + 33140.0000 0.4000 21.7700 + 33140.0000 1.9000 20.7600 + 33140.0000 2.7000 20.5100 + 33140.0000 4.2000 19.1500 rg + 33140.0000 4.3000 19.0600 + 33140.0000 6.4000 17.9200 + 33140.0000 11.3000 17.9200 + 33140.0000 12.6000 19.1400 rd + 33140.0000 18.9000 21.8700 + 33140.0000 19.2000 22.1800 + 999.9990 999.9990 999.9990 + 20 0 0 10 33425.0000 + 33425.0000 0.0000 21.6800 + 33425.0000 0.5000 21.5300 + 33425.0000 2.2000 20.1600 + 33425.0000 3.0000 19.9400 + 33425.0000 4.8000 18.8200 rg + 33425.0000 4.9000 18.7500 + 33425.0000 7.2000 17.8000 + 33425.0000 11.7000 17.8000 + 33425.0000 12.8000 18.8800 rd + 33425.0000 18.2000 21.6800 + 999.9990 999.9990 999.9990 + 21 0 0 10 33685.0000 + 33685.0000 0.0000 21.5300 + 33685.0000 0.5000 21.3200 + 33685.0000 2.4000 19.6200 + 33685.0000 3.3000 19.4200 + 33685.0000 5.2000 18.5300 rg + 33685.0000 5.4000 18.4700 + 33685.0000 7.9000 17.7000 + 33685.0000 12.0000 17.7000 + 33685.0000 12.9000 18.6500 rd + 33685.0000 17.6000 21.5300 + 999.9990 999.9990 999.9990 + 22 0 0 9 33920.0000 + 33920.0000 0.6000 20.9800 + 33920.0000 2.6000 19.1300 + 33920.0000 3.6000 18.9400 + 33920.0000 5.7000 18.2600 rg + 33920.0000 5.8000 18.2200 + 33920.0000 8.5000 17.6000 + 33920.0000 12.3000 17.6000 + 33920.0000 13.1000 18.4400 rd + 33920.0000 17.0000 21.1300 + 999.9990 999.9990 999.9990 + 23 0 0 9 34095.0000 + 34095.0000 0.6000 20.9500 + 34095.0000 2.7000 18.7700 + 34095.0000 3.8000 18.5900 + 34095.0000 6.0000 18.0600 rg + 34095.0000 6.1000 18.0300 + 34095.0000 9.0000 17.5300 + 34095.0000 12.5000 17.5300 + 34095.0000 13.2000 18.2800 rd + 34095.0000 16.6000 20.9800 + 999.9990 999.9990 999.9990 + 24 0 0 10 34295.0000 + 34295.0000 0.0000 21.3600 + 34295.0000 0.6000 20.8300 + 34295.0000 2.4000 18.3500 + 34295.0000 3.5000 18.1900 + 34295.0000 5.8000 17.8300 rg + 34295.0000 6.5000 17.3400 + 34295.0000 9.5000 17.3600 + 34295.0000 12.8000 17.4100 + 34295.0000 14.0000 18.1000 rd + 34295.0000 16.1000 21.3600 + 999.9990 999.9990 999.9990 + 25 0 0 10 34334.0000 amontPontThivencel + 34334.0000 0.0000 21.3600 + 34334.0000 0.6000 20.8300 + 34334.0000 2.4000 18.3500 + 34334.0000 3.5000 18.1900 + 34334.0000 5.8000 17.8300 rg + 34334.0000 6.5000 17.3400 + 34334.0000 9.5000 17.3600 + 34334.0000 12.8000 17.4100 + 34334.0000 14.0000 18.1000 rd + 34334.0000 16.1000 21.1300 + 999.9990 999.9990 999.9990 + 26 0 0 9 34335.0000 PontmThivencelle + 34335.0000 2.9000 22.0000 + 34335.0000 2.9000 20.7400 + 34335.0000 2.9000 18.2900 rg + 34335.0000 6.4000 17.7900 + 34335.0000 9.6000 17.2600 + 34335.0000 12.8000 17.2600 + 34335.0000 12.8000 18.3100 rd + 34335.0000 12.8000 20.7400 + 34335.0000 12.8000 22.0000 + 999.9990 999.9990 999.9990 + 27 0 0 9 34343.0000 PontvThivencelle + 34343.0000 2.9000 22.0000 + 34343.0000 2.9000 20.7400 + 34343.0000 2.9000 18.2900 rg + 34343.0000 6.4000 17.7900 + 34343.0000 9.6000 17.2600 + 34343.0000 12.8000 17.2600 + 34343.0000 12.8000 18.2900 rd + 34343.0000 12.8000 20.7400 + 34343.0000 12.8000 22.0000 + 999.9990 999.9990 999.9990 + 28 0 0 10 34345.0000 avalPontThivencell + 34345.0000 0.0000 21.3600 + 34345.0000 0.6000 20.8300 + 34345.0000 2.4000 18.3500 + 34345.0000 3.5000 18.1900 + 34345.0000 5.8000 17.8300 rg + 34345.0000 6.5000 17.3400 + 34345.0000 9.5000 17.2600 + 34345.0000 12.8000 17.4100 + 34345.0000 14.0000 18.1000 rd + 34345.0000 16.1000 21.1300 + 999.9990 999.9990 999.9990 + 29 0 0 7 34405.0000 + 34405.0000 0.0000 21.1400 + 34405.0000 2.9000 17.8400 rg + 34405.0000 6.4000 17.4400 + 34405.0000 9.6000 17.2000 + 34405.0000 12.8000 17.2200 + 34405.0000 13.9000 17.8400 rd + 34405.0000 19.0000 21.0000 + 999.9990 999.9990 999.9990 + 30 0 0 7 34535.0000 + 34535.0000 0.0000 20.5900 + 34535.0000 2.9000 17.7400 rg + 34535.0000 6.4000 17.2400 + 34535.0000 9.6000 17.0300 + 34535.0000 12.8000 17.0300 + 34535.0000 13.5000 17.7400 rd + 34535.0000 15.8000 20.6000 + 999.9990 999.9990 999.9990 + 31 0 0 7 34705.0000 + 34705.0000 0.0000 20.5000 + 34705.0000 2.9000 17.6100 rg + 34705.0000 6.4000 17.1100 + 34705.0000 9.6000 16.8500 + 34705.0000 12.8000 16.8500 + 34705.0000 14.0000 18.0000 rd + 34705.0000 16.2000 20.5300 + 999.9990 999.9990 999.9990 + 32 0 0 7 34825.0000 + 34825.0000 0.0000 20.6200 + 34825.0000 2.9000 17.5200 rg + 34825.0000 6.4000 17.0200 + 34825.0000 9.6000 16.7100 + 34825.0000 12.8000 16.7100 + 34825.0000 13.5000 17.5200 rd + 34825.0000 16.2000 20.5000 + 999.9990 999.9990 999.9990 + 33 0 0 7 34995.0000 + 34995.0000 0.0000 20.3900 + 34995.0000 2.9000 17.3900 rg + 34995.0000 6.4000 16.8900 + 34995.0000 9.6000 16.5300 + 34995.0000 12.8000 16.5300 + 34995.0000 13.6000 17.4300 rd + 34995.0000 16.2000 20.3200 + 999.9990 999.9990 999.9990 + 34 0 0 7 35135.0000 + 35135.0000 0.0000 20.1600 + 35135.0000 2.9000 17.2900 rg + 35135.0000 6.4000 16.7900 + 35135.0000 9.6000 16.4300 + 35135.0000 12.8000 16.4300 + 35135.0000 13.9000 17.3700 rd + 35135.0000 16.2000 20.0600 + 999.9990 999.9990 999.9990 + 35 0 0 7 35360.0000 + 35360.0000 0.0000 19.8600 + 35360.0000 2.9000 17.1200 rg + 35360.0000 6.4000 16.6200 + 35360.0000 9.6000 16.2600 + 35360.0000 12.8000 16.2600 + 35360.0000 13.7000 17.1200 rd + 35360.0000 16.2000 19.4600 + 999.9990 999.9990 999.9990 + 36 0 0 7 35580.0000 + 35580.0000 0.0000 20.0000 + 35580.0000 2.9000 16.9500 rg + 35580.0000 3.0000 16.0900 + 35580.0000 11.5000 16.0500 + 35580.0000 20.0000 16.0900 + 35580.0000 21.0000 17.0300 rd + 35580.0000 23.0000 19.5000 + 999.9990 999.9990 999.9990 + 37 0 0 7 35920.0000 + 35920.0000 0.0000 20.0000 + 35920.0000 2.2000 16.0200 rg + 35920.0000 3.0000 14.5500 + 35920.0000 11.5000 14.5000 + 35920.0000 20.0000 14.5500 + 35920.0000 20.9000 16.0200 rd + 35920.0000 23.0000 19.5000 + 999.9990 999.9990 999.9990 + 38 0 0 7 36270.0000 S1 + 36270.0000 0.0000 20.0000 + 36270.0000 1.8000 17.0100 rg + 36270.0000 3.0000 15.0800 + 36270.0000 11.5000 15.0500 + 36270.0000 20.0000 15.0800 + 36270.0000 21.3000 17.0100 rd + 36270.0000 23.0000 19.5000 + 999.9990 999.9990 999.9990 + 39 0 0 7 36271.0000 S1bis + 36271.0000 0.0000 20.0000 + 36271.0000 1.8000 17.0100 rg + 36271.0000 3.0000 13.0800 + 36271.0000 11.5000 13.0500 + 36271.0000 20.0000 13.0800 + 36271.0000 21.3000 17.0100 rd + 36271.0000 23.0000 19.5000 + 999.9990 999.9990 999.9990 diff --git a/doc/users/Tuto1/data/Cote_canal b/doc/users/Tuto1/data/Cote_canal new file mode 100644 index 00000000..f7607c35 --- /dev/null +++ b/doc/users/Tuto1/data/Cote_canal @@ -0,0 +1,2 @@ +0:00:00:00 15 +0:00:01:00 15 diff --git a/doc/users/Tuto1/data/Fevrier_2002 b/doc/users/Tuto1/data/Fevrier_2002 new file mode 100644 index 00000000..3344d84c --- /dev/null +++ b/doc/users/Tuto1/data/Fevrier_2002 @@ -0,0 +1,25 @@ +0:00:00:00 5,53 +0:11:37:00 5,72 +0:16:17:00 5,82 +0:22:57:00 5,92 +1:00:00:00 6,6 +1:00:57:00 8,04 +1:02:29:00 14,3 +1:04:38:00 24,4 +1:08:48:00 40 +1:11:27:00 46,3 +1:13:47:00 48,8 +1:16:07:00 47,9 +1:17:17:00 44,8 +1:20:23:00 27,8 +1:21:57:00 24,3 +2:01:15:00 19,2 +2:05:15:00 15,8 +2:11:25:00 11,5 +2:11:57:00 11,1 +2:13:17:00 10,5 +2:19:07:00 9,84 +2:23:59:00 8,43 +3:04:51:00 7,02 +3:09:43:00 5,61 +3:14:35:00 5,53 diff --git a/doc/users/Tuto1/data/Hogneau.SIN b/doc/users/Tuto1/data/Hogneau.SIN new file mode 100644 index 00000000..53721ef1 --- /dev/null +++ b/doc/users/Tuto1/data/Hogneau.SIN @@ -0,0 +1,25 @@ +**************** SINGULARITES ********************** +*BieSec Nb Longueur Cote dev Cote charg Coef deb Impression +*===---===----------==========----------==========----------------------------- +***************************************************** +* Bief n° 1 +***************************************************** +* Ouvrage au pk 36270.0 seuil_aval +*SeuilR type D +D 1 36270.00 18.250 15.500 0.000 0.400 99999.000 seuil palplanches +* Ouvrage au pk 31875.0 PontRS101 +*OrificeR type O +O 1 31875.00 9.660 18.710 21.890 0.400 9999.000 +*SeuilR type D +D 1 31875.00 14.000 22.890 0.000 0.400 99999.000 +* Ouvrage au pk 34335.0 PontThivencelle +*SeuilR type D +D 1 34335.00 13.000 21.740 0.000 0.400 99999.000 +*OrificeR type O +O 1 34335.00 9.070 17.280 20.740 0.400 9999.000 +***************************************************** +* Bief n° 3 +***************************************************** +* Ouvrage au pk 0.0 Orifice +*OrificeR type O +*O 3 0.000 6.000 19.000 19.001 0.400 20.000 Clapet_001 diff --git a/doc/users/Tuto1/data/Hogneau.var b/doc/users/Tuto1/data/Hogneau.var new file mode 100644 index 00000000..33b537e6 --- /dev/null +++ b/doc/users/Tuto1/data/Hogneau.var @@ -0,0 +1 @@ +$Clapet_001 clapet \ No newline at end of file diff --git a/doc/users/Tuto1/data/Q_permanent b/doc/users/Tuto1/data/Q_permanent new file mode 100644 index 00000000..23060f86 --- /dev/null +++ b/doc/users/Tuto1/data/Q_permanent @@ -0,0 +1,2 @@ +0:00:00:00 4 +0:00:01:00 4 diff --git a/doc/users/Tuto1/data/Seuil_aval_23m b/doc/users/Tuto1/data/Seuil_aval_23m new file mode 100644 index 00000000..c683ca7e --- /dev/null +++ b/doc/users/Tuto1/data/Seuil_aval_23m @@ -0,0 +1,24 @@ +16,03 0 +16,13 1,29 +16,23 3,64 +16,33 6,7 +16,43 10,31 +16,53 14,41 +16,63 18,94 +16,73 23,87 +16,83 29,16 +16,93 34,79 +17,03 40,75 +17,13 47,01 +17,23 53,57 +17,33 60,4 +17,43 67,5 +17,53 74,86 +17,63 82,47 +17,73 90,33 +17,83 98,41 +17,93 106,73 +18,03 115,26 +18,13 124,01 +18,23 132,98 +18,33 142,14 diff --git a/doc/users/Tuto1/data/T_100ans b/doc/users/Tuto1/data/T_100ans new file mode 100644 index 00000000..a542d34d --- /dev/null +++ b/doc/users/Tuto1/data/T_100ans @@ -0,0 +1,9 @@ +0:00:00:00 4 +0:01:16:48 4 +0:16:00:00 50 +1:11:46:28 36,8 +2:07:27:22 23,3 +5:04:24:58 13,8 +10:03:00:06 9,38 +20:02:19:58 7,29 +30:01:54:37 5,97 diff --git a/doc/users/Tuto1/data/T_10ans b/doc/users/Tuto1/data/T_10ans new file mode 100644 index 00000000..495627ff --- /dev/null +++ b/doc/users/Tuto1/data/T_10ans @@ -0,0 +1,10 @@ +0:00:00:00 4 +0:01:49:48 4 +0:16:00:00 35 +1:11:25:48 25 +2:07:30:00 16,4 +5:03:55:12 9,66 +10:03:00:00 6,56 +27:00:54:00 4,91 +30:01:48:36 4 +31:06:00:00 4 diff --git a/doc/users/Tuto1/dl.png b/doc/users/Tuto1/dl.png new file mode 100644 index 00000000..40f6158c Binary files /dev/null and b/doc/users/Tuto1/dl.png differ diff --git a/doc/users/Tuto1/editsect.png b/doc/users/Tuto1/editsect.png new file mode 100644 index 00000000..2199cb4b Binary files /dev/null and b/doc/users/Tuto1/editsect.png differ diff --git a/doc/users/Tuto1/fev2002.png b/doc/users/Tuto1/fev2002.png new file mode 100644 index 00000000..9002213a Binary files /dev/null and b/doc/users/Tuto1/fev2002.png differ diff --git a/doc/users/Tuto1/frictions.png b/doc/users/Tuto1/frictions.png new file mode 100644 index 00000000..2f66cc73 Binary files /dev/null and b/doc/users/Tuto1/frictions.png differ diff --git a/doc/users/Tuto1/gtk_add.png b/doc/users/Tuto1/gtk_add.png new file mode 100755 index 00000000..90b449e4 Binary files /dev/null and b/doc/users/Tuto1/gtk_add.png differ diff --git a/doc/users/Tuto1/ic.png b/doc/users/Tuto1/ic.png new file mode 100644 index 00000000..5a7e86a2 Binary files /dev/null and b/doc/users/Tuto1/ic.png differ diff --git a/doc/users/Tuto1/main.png b/doc/users/Tuto1/main.png new file mode 100644 index 00000000..eba19fa4 Binary files /dev/null and b/doc/users/Tuto1/main.png differ diff --git a/doc/users/Tuto1/network.png b/doc/users/Tuto1/network.png new file mode 100644 index 00000000..29d33e9b Binary files /dev/null and b/doc/users/Tuto1/network.png differ diff --git a/doc/users/Tuto1/step-by-step.tex b/doc/users/Tuto1/step-by-step.tex new file mode 100644 index 00000000..1274088b --- /dev/null +++ b/doc/users/Tuto1/step-by-step.tex @@ -0,0 +1,267 @@ +%% LyX 2.0.2 created this file. For more info, see http://www.lyx.org/. +%% Do not edit unless you really know what you are doing. +\documentclass[12pt,french]{article} +\usepackage[T1]{fontenc} +\usepackage[latin9]{inputenc} +\usepackage{geometry} +\geometry{verbose,tmargin=2cm,bmargin=2cm,lmargin=2cm,rmargin=2cm,headheight=2cm,headsep=2cm,footskip=2cm} +\usepackage{textcomp} +\usepackage{graphicx} +\usepackage{hyperref} + +\makeatletter + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% LyX specific LaTeX commands. +\newcommand{\noun}[1]{\textsc{#1}} +%% Because html converters don't know tabularnewline +\providecommand{\tabularnewline}{\\} + +\makeatother + +\usepackage{babel} +\addto\extrasfrench{% + \providecommand{\og}{\leavevmode\flqq~}% + \providecommand{\fg}{\ifdim\lastskip>\z@\unskip\fi~\frqq}% +} + +\begin{document} +\includegraphics[width=5cm]{Logo-INRAE_Transparent.png} + +~ + +~ + +~ + +\begin{center} +Tutorial for Pamhyr2 + +January 2024 +\par\end{center} + +~ + +~ + +\begin{center} +\textbf{\LARGE 1D Modeling of the Hogneau River (Nord, France)} + +using Pamhyr2 +\par\end{center}{\LARGE \par} + +~ + +~ + +\begin{center} +{\large INRAE Lyon-Grenoble Auvergne-Rhône-Alpes} +\par\end{center}{\large \par} + +\begin{center} +Riverly, river hydraulics +\par\end{center} + +~ + +~ + +~ + +~ + +~ + +~ + +\begin{center} +\begin{tabular}{lll} +Autors : & Pierre-Antoine Rouby & pierre-antoine.rouby@inrae.fr\tabularnewline +& Théophile TERRAZ & theophile.terraz@inrae.fr\tabularnewline +\end{tabular} +\par\end{center} + +~ + +\pagebreak{} + +\begin{center} +\tableofcontents{} +\par\end{center} + +~ + +\pagebreak{} + +\section{Introduction} + +TODO + +\pagebreak{} + +\section{Install Pamhyr2} + +Pamhyr2 can be downloaded from \url{https://gitlab.irstea.fr/theophile.terraz/pamhyr}. +\begin{center} +\includegraphics[width=15cm]{dl.png} +\par\end{center} + +Use the GNU Linux or the Windows download button depending on your system. On windows, launch the installer. On Linux, unpack the archive and launch Pamhyr2. + + +\section{Create your first study} + +On the main windows, click on \texttt{[Files] => [New Study]} to create a new study. + +Click on \texttt{[River Network] => [Edit River Network]} to create the reaches of your river. In this window, you must define an oriented graph that represents the reaches of your river network: the edges are the reaches and the nodes are either upstream boundary conditions, downstream boundary conditions or junctions. +Press the \includegraphics[width=0.5cm]{gtk_add.png} to enter the \textit{add} mode. Create two nodes by clicking in the grey zone of the window, and create a link by clicking again on each node. press \includegraphics[width=0.5cm]{gtk_add.png} again to exit the \textit{add} mode. You created your first reach, with an upstream node and a downstream node. In the lower part of the \textit{Edit River Network} window you can rename the nodes and the reaches. As the reach you created is automaticaly selected, all the next steps will apply to this reach. The window should look like that: + +\begin{center} +\includegraphics[width=15cm]{network.png} +\par\end{center} + +Close the \textit{Edit River Network} window. + + +\section{Edit the river geometry} + +Click on \texttt{[Geometry] => [Edit Geometry]} to define the geometry of the selected bief. Click on the \texttt{[Import]} button and select the file \texttt{Data/Bief\_1.st}. You should see: + +\begin{center} +\includegraphics[width=15cm]{Geo.png} +\par\end{center} + +On the left panel is a list of all the cross sections with their name and longitudinal abscisa. In the top left plot you can see the top view of the river, on the top right panel the longitudinal cross-section of the river and in the bottom plot you can see the selected cross-section (blue) along with the next one (dashed purple) and previous one (dashed black). You can move in the section list using the slider at the very right of the window. + +You can edit the selected cross section by clicking on the \includegraphics[width=0.5cm]{"../../../src/View/ui/ressources/edit.png"} icon. + +select the cross section named \textit{PontRD101m} and open the edition window. You should see: + +\begin{center} +\includegraphics[width=15cm]{editsect.png} +\par\end{center} + +On the left panel is the list of all the points of the section, with their coordinates, their name and their transversal absisa. The Z coordinate of the highest point is written in blue and the lowest in red. Points can have a name. If a point with the same name exists in every section in a reach, it forms a longitudinal line. For example, here we have \textit{rg} and \textit{rd} which represent the left bank and the right bank of the main chanel. + +On the plot is a projection of the cross section. You can use \texttt{[ctrl + click]} to select a point in the plot and \texttt{[shift + click]} to select a water line and fisualize usefull geometric data. You can close the cross section edition window and the geometry edition window. + +\section{Edit the boundary conditions} + +From the main window, click on \texttt{[Hydraulics] => [Boundary conditions and punctual contributions]}.You should see: + +\begin{center} +\includegraphics[width=15cm]{boundary.png} +\par\end{center} + + Use the \textit{add} button on the top left of the window to add a liquid boundary condition. + On the new line, click to select the whole line, double click to select the cell. + Select the \textit{Type} cell to give a name to the boundary condition. here, we will define the flow discharge mesured during the february 2002 flood. You can name this boundary condition "flood2002". + Select the \textit{Type} cell and use the combo box to put a Q(t) law. + Select the \textit{Node} cell and atribute this condition to the upstream node. + Names of the nodes are recalled in network in the right panel. + Now select the whole line and click on the edit button \includegraphics[width=0.5cm]{"../../../src/View/ui/ressources/edit.png"}. You opened the \textit{Edit Boundary Conditions} window. + On a text editor, open the \texttt{Data/Fevrier\_2002.txt} file. Copy the content of the file (for example with \textit{ctrl+a ctrl+c}) and paste it in the left panel of the \textit{Edit Boundary Conditions} window with \textit{ctrl+v}. You can now see the flow discharge curve: + +\begin{center} +\includegraphics[width=15cm]{fev2002.png} +\par\end{center} + +Close this window. Go back on the \textit{Boundary Conditions} window. Add a new line, give it a name, give it the textit{Z(T)} type and associate it to the downstream node of the network. Open the \textit{Edit Boundary Conditions} window (\includegraphics[width=0.5cm]{"../../../src/View/ui/ressources/edit.png"}). Add two lines. In the first one, enter time: 0.00.00 and Z: 15.000. +On the second one, time: 1.00.00 and Z: 15.000. It creates a constant downstream water elevation. For the computaion, Mage will extrapolate continuously the water elevation, that's why we only need to define one hour. +You can close the the \textit{Edit Boundary Conditions} and the \textit{Boundary Conditions} window. + +\section{Create initial conditions} + +From the main window, click on \texttt{[Hydraulics] => [Initial conditions]}. +If you don't know the initial conditions in water elevation and flow discharge of the river, you can use \texttt{[Generate minimal height]} or \texttt{[Generate constant discharge]} buttons to let Pamhyr2 estimate an initial condition using the Manning-Strickler formula. +Click on \texttt{[Generate minimal height]} and enter a discharge of $4 m^2$ in the pop-up window to generate an initial water height condition based on the Manning-Strickler formula and a uniform discharge of $4 m^2$. You should see: + +\begin{center} +\includegraphics[width=15cm]{ic.png} +\par\end{center} + +Close the \textit{Initial conditions} window. + + +\section{Edit friction coefficients} + +From the main window, click on \texttt{[Hydraulics] => [Edit friction]}. +You fist have to define sets of Strickler coefficients. Click on \includegraphics[width=0.5cm]{"../../../src/View/ui/ressources/edit.png"} to open the \textit{Strickler} window. Here you can create couples of Strickler coefficients, the first one for the minor bed, the second one for the medium bed. Click on \textit{add} four times to create four new couple. Give them the folowing values: + +\begin{center} +\includegraphics[width=15cm]{K.png} +\par\end{center} + +Close the \textit{Strickler} window. On the \textit{Edit friction} window, add four lines with the button \includegraphics[width=0.5cm]{gtk_add.png} to create four friction zones. Each zone is defined by a \textit{begin} and \textit{end} PK and a \textit{begin} and \textit{end} Strickler couple. The strickler coefficient couples inside a zone are interpolated from the \textit{begin} and \textit{end} couples. In our case, we will use constant coefficients per zone. Set the zones as follow: + +\begin{center} +\includegraphics[width=15cm]{frictions.png} +\par\end{center} + +The selected zone is highlighted in blue. Close the \textit{Edit friction} window. + + +\section{Model hydraulic structures} + +Sometimes there can be cross-sections in which Shallow water equations can not be used to model the water flow. In that case, we have to define an other law to link the water elevation and the flow discharge. This is the case, for example, under bridges when the water elevation is too high, leading to a flow in charge. Pamhyr2 enables to define various hydraulic structures with laws that can be parametrized. In our case, a weir and two bridges have to be represented as hydraulic structures. +From the main window, click on \texttt{[Hydraulics] => [Hydraulic structures]} to open the hydraulic structures window. Click tree times on the \includegraphics[width=0.5cm]{gtk_add.png} button to create three hydraulic structures. Each structure can have a name and must have a reach and a kp. Set them as follow: + +\begin{center} +\includegraphics[width=15cm]{hs.png} +\par\end{center} + +Select the downstream weir and click on \includegraphics[width=0.5cm]{"../../../src/View/ui/ressources/edit.png"} to edit the laws of this structure. Hydraulic structures are composed of basic hydraulic structures. You can combine the laws of several basic hydraulic structures to setup your structure. For this weir, we only need a weir basic hydraulic structure. Click on \includegraphics[width=0.5cm]{gtk_add.png} to add a new basic hydraulic structure, give it the \textit{weir} type and set it up as folow: + +\begin{center} +\includegraphics[width=15cm]{seuil.png} +\par\end{center} + +Go back to the \textit{hydraulic structures} window. + +Select the RS101 bridge and click on \includegraphics[width=0.5cm]{"../../../src/View/ui/ressources/edit.png"} to edit the laws of this structure. A bridge can be modeled as a combination of an orifice for the flow under the bridge and a weir for the flow over the bridge. Create two basic hydraulic structures and set them as folow: + +\begin{center} +\includegraphics[width=15cm]{seuilRS101.png} + +\includegraphics[width=15cm]{orificeRS101.png} +\par\end{center} + +Go back to the \textit{hydraulic structures} window and aply the same procedure for the Thivencelle bridge: + +\begin{center} +\includegraphics[width=15cm]{orificeThivencelle.png} + +\includegraphics[width=15cm]{seuilThivencelle.png} +\par\end{center} + +You can now close the \textit{hydraulic structures} windows. + +\section{Solver parameters} + +From the main window, click on \texttt{[Execute] => [Numerical parameters for solvers]}. +In the window \textit{solver parameters} select the \textit{Mage v8} tab. Set the solver parameters as follow: + +\begin{center} +\includegraphics[width=15cm]{param.png} +\par\end{center} + +Close the \textit{solver parameters} window. + +\section{Run the simulation} + +From the main window, click on \texttt{[Execute] => [Run solver]}. +Select \textit{Defaut-Mage - (Mage8)} anc click on the \textit{Run} button. +It will open two windows: the \textit{Check list} window and the \textit{Solver log} window. +The \textit{Check list} window gives som hints about the validity of your model, and the \textit{Solver log} window displays the outputs of the solver. +From the \textit{Solver log} window you can re-run the computation, and from the textit{Solver log} window you can click on the \textit{Results} button to open the \textit{Results} window. + +\section{Visualize the results} + +If you closed the textit{Solver log} window, you can click on \texttt{[Results] => [Visualize last results]} from the main window to open the \textit{Results} window. The top lets panel let you select your reach, the bottom left panel lets you select a cross-section in that reach. the tree plots on the right show the reach and the cross-section the same way than in the \textit{Geometry} window. You can use the bottom slider to visualize the water elevation at different timesteps. +To visualize the flow discharge, switch to the \textit{Hydrograph} tab. +To create custom 2D plots, click on the \includegraphics[width=0.5cm]{gtk_add.png} button on the top left of the window. Select the values you want on the $X$ and $Y$ axis and click on \textit{OK}. +You can now see a new tab with the custom 2D plot in the right panel of the \textit{Results} window. + + +\pagebreak{} +\end{document} + diff --git a/doc/users/build.sh b/doc/users/build.sh index 476dd5ad..2e8deb56 100755 --- a/doc/users/build.sh +++ b/doc/users/build.sh @@ -1,10 +1,18 @@ #! /bin/sh -# Build the PDF - -emacs -Q -q -nw --batch \ +echo " PDF documentation.org" +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-latex-export-to-pdf + +echo " HTML documentation.org" +emacs -nw --batch \ + --eval "(require 'org)" \ + --eval "(require 'ox-latex)" \ + --eval "(require 'ox-html)" \ + --eval "(load-file \"../tools/setup.el\")" \ + --file ./documentation.org \ + -f org-html-export-to-html diff --git a/doc/users/images/.gitkeep b/doc/users/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/full-requirements.txt b/full-requirements.txt new file mode 100644 index 00000000..efc9f6d8 --- /dev/null +++ b/full-requirements.txt @@ -0,0 +1,12 @@ +PyQt5==5.15.4 +PyQt5-Qt5==5.15.2 +PyQt5-sip==12.12.2 +PyQtWebEngine==5.15.6 +QScintilla>=2.14.1 +pyqtgraph>=0.12.1 +matplotlib>=3.4.1 +numpy>=1.24.2 +colorama>=0.4.3 +pyinstaller>=5.11.0 +shapely>=2.0.1 +lxml>=4.9.3 diff --git a/packages/linux.sh b/packages/linux.sh index 215047e8..b6177eb4 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -25,6 +25,7 @@ echo " *** SETUP ENV" python3 -m venv linux-pamhyr-venv . linux-pamhyr-venv/bin/activate +pip3 install -U pip pip3 install -r ../requirements.txt echo " *** PYINSTALL" @@ -43,18 +44,22 @@ cd $OLD_PWD echo " *** COPY DATA" -mkdir -p dist/pamhyr/View/ui -mkdir -p dist/pamhyr/View/ui/Widgets -cp -r ../src/View/ui/ressources/ dist/pamhyr/View/ui/ -cp -r ../src/View/ui/Widgets/*.ui dist/pamhyr/View/ui/Widgets/ -cp -r ../src/View/ui/*.ui dist/pamhyr/View/ui/ +mkdir -p dist/pamhyr/_internal/View/ui +mkdir -p dist/pamhyr/_internal/View/ui/Widgets +cp -r ../src/View/ui/ressources/ dist/pamhyr/_internal/View/ui/ +cp -r ../src/View/ui/Widgets/*.ui dist/pamhyr/_internal/View/ui/Widgets/ +cp -r ../src/View/ui/*.ui dist/pamhyr/_internal/View/ui/ mkdir -p dist/pamhyr/lang cp -r ../src/lang/*.qm dist/pamhyr/lang/ -cp ../VERSION dist/pamhyr/ -cp ../AUTHORS dist/pamhyr/ -cp ../LICENSE dist/pamhyr/ +mkdir dist/pamhyr/_internal/ +#cp ../VERSION dist/pamhyr/_internal/ +VERSION=$(cat ../VERSION) +echo "local-build-$VERSION" > dist/pamhyr/_internal/VERSION +cp ../AUTHORS dist/pamhyr/_internal/ +cp ../LICENSE dist/pamhyr/_internal/ +cp ../src/motd.txt dist/pamhyr/_internal/ mkdir -p dist/pamhyr/mage/ cp ../mage/mage dist/pamhyr/mage/ @@ -65,6 +70,18 @@ mkdir -p dist/pamhyr/tests_cases/ mkdir -p dist/pamhyr/tests_cases/Saar cp ../tests_cases/Saar/Saar.pamhyr dist/pamhyr/tests_cases/Saar/ +mkdir -p dist/pamhyr/doc/ +cp ../doc/dev/documentation.pdf dist/pamhyr/doc/Pamhyr2-dev.pdf +cp ../doc/dev/documentation.html dist/pamhyr/doc/Pamhyr2-dev.html + +cp ../doc/users/documentation.pdf dist/pamhyr/doc/Pamhyr2-users.pdf +cp ../doc/users/documentation.html dist/pamhyr/doc/Pamhyr2-users.html + +mkdir -p dist/pamhyr/doc/images +cp ../doc/users/images/* dist/pamhyr/doc/images/ +cp ../doc/dev/images/* dist/pamhyr/doc/images/ +cp ../doc/images/* dist/pamhyr/doc/images/ + echo " *** MAKE SRC PACKAGE" OLD_PWD=$PWD @@ -76,11 +93,11 @@ mv ../pamhyr-src.tar.gz ./ echo " *** MAKE BIN PACKAGE" -OLD_PWD=$PWD -cd dist/ -tar --xz -cf pamhyr-gnulinux-amd64.tar.xz pamhyr --checkpoint=.100 -cd $OLD_PWD +# OLD_PWD=$PWD +# cd dist/ +# tar --xz -cf pamhyr-gnulinux-amd64.tar.xz pamhyr --checkpoint=.100 +# cd $OLD_PWD -mv dist/pamhyr-gnulinux-amd64.tar.xz ./ +# mv dist/pamhyr-gnulinux-amd64.tar.xz ./ echo " *** DONE" diff --git a/packages/make-windows-dir.bat b/packages/make-windows-dir.bat index 1045b4d4..28eccbc4 100644 --- a/packages/make-windows-dir.bat +++ b/packages/make-windows-dir.bat @@ -16,26 +16,27 @@ rem along with this program. If not, see . @ECHO ON -mkdir pamhyr\pamhyr +mkdir pamhyr\pamhyr\_internal xcopy /s /e /y dist\pamhyr pamhyr\pamhyr rem Copy data -mkdir pamhyr\pamhyr\lang -mkdir pamhyr\pamhyr\View\ui\ressources -mkdir pamhyr\pamhyr\View\ui\Widgets +mkdir pamhyr\pamhyr\_internal\lang +mkdir pamhyr\pamhyr\_internal\View\ui\ressources +mkdir pamhyr\pamhyr\_internal\View\ui\Widgets rem UI -copy /y ..\src\View\ui\ressources\ pamhyr\pamhyr\View\ui\ressources -copy /y ..\src\View\ui\Widgets\*.ui pamhyr\pamhyr\View\ui\Widgets -copy /y ..\src\View\ui\*.ui pamhyr\pamhyr\View\ui\ +copy /y ..\src\View\ui\ressources\ pamhyr\pamhyr\_internal\View\ui\ressources +copy /y ..\src\View\ui\Widgets\*.ui pamhyr\pamhyr\_internal\View\ui\Widgets +copy /y ..\src\View\ui\*.ui pamhyr\pamhyr\_internal\View\ui\ rem Lang -copy /y ..\src\lang\*.qm pamhyr\pamhyr\lang\ +copy /y ..\src\lang\*.qm pamhyr\pamhyr\_internal\lang\ rem Information -copy /y ..\VERSION pamhyr\pamhyr -copy /y ..\AUTHORS pamhyr\pamhyr -copy /y ..\LICENSE pamhyr\pamhyr +copy /y ..\VERSION pamhyr\pamhyr\_internal +copy /y ..\AUTHORS pamhyr\pamhyr\_internal +copy /y ..\LICENSE pamhyr\pamhyr\_internal +copy /y ..\src\motd.txt pamhyr\pamhyr\_internal rem MAGE mkdir pamhyr\mage @@ -50,5 +51,11 @@ copy /y ..\tests_cases\Saar\Saar.pamhyr pamhyr\tests_cases\Saar\ rem Documentations mkdir pamhyr\doc +mkdir pamhyr\doc\images copy /y ..\doc\users\documentation.pdf pamhyr\doc\Pamhyr2-users.pdf copy /y ..\doc\dev\documentation.pdf pamhyr\doc\Pamhyr2-dev.pdf +copy /y ..\doc\users\documentation.html pamhyr\doc\Pamhyr2-users.html +copy /y ..\doc\dev\documentation.html pamhyr\doc\Pamhyr2-dev.html +copy /y ..\doc\users\images\* pamhyr\doc\images +copy /y ..\doc\dev\images\* pamhyr\doc\images +copy /y ..\doc\images\* pamhyr\doc\images diff --git a/packages/pamhyr.nsi b/packages/pamhyr.nsi index 80ad4770..c4591745 100644 --- a/packages/pamhyr.nsi +++ b/packages/pamhyr.nsi @@ -5,7 +5,7 @@ Name "Pamhyr2" OutFile "pamhyr-windows.exe" -LicenseData "pamhyr\pamhyr\LICENSE" +LicenseData "pamhyr\pamhyr\_internal\LICENSE" LicenseText "I Agree" RequestExecutionLevel admin diff --git a/requirements.txt b/requirements.txt index 729dbbe4..23b82bb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ PyQt5==5.15.4 PyQt5-Qt5==5.15.2 -PyQt5-sip==12.8.1 +PyQt5-sip==12.12.2 +#PyQtWebEngine==5.15.6 +QScintilla>=2.14.1 pyqtgraph>=0.12.1 matplotlib>=3.4.1 numpy>=1.24.2 colorama>=0.4.3 pyinstaller>=5.11.0 shapely>=2.0.1 +lxml>=4.9.3 diff --git a/src/Checker/Checker.py b/src/Checker/Checker.py index d10a8c4d..e97e0b04 100644 --- a/src/Checker/Checker.py +++ b/src/Checker/Checker.py @@ -20,12 +20,14 @@ from enum import Enum from Model.Except import NotImplementedMethodeError + class STATUS(Enum): UNKNOWN = -1 OK = 0 WARNING = 1 ERROR = 2 + class AbstractModelChecker(object): def __init__(self): self._name = "" @@ -62,6 +64,12 @@ class AbstractModelChecker(object): # Abstract function + def _run(self, study): + from Model.Study import Study + + thread_study = Study.open(study._filename) + self.run(thread_study) + def run(self, study): """Run checker function diff --git a/src/Checker/Mage.py b/src/Checker/Mage.py index edb0c349..d5b527fa 100644 --- a/src/Checker/Mage.py +++ b/src/Checker/Mage.py @@ -27,8 +27,9 @@ from Checker.Checker import AbstractModelChecker, STATUS _translate = QCoreApplication.translate + class MageNetworkGraphChecker(AbstractModelChecker): - def __init__(self, connectivity = True): + def __init__(self, connectivity=True): super(MageNetworkGraphChecker, self).__init__() self._mode_conn = connectivity @@ -38,8 +39,10 @@ class MageNetworkGraphChecker(AbstractModelChecker): else: mode = "cycle" - self._name = _translate("Checker", f"Mage network graph {mode} checker") - self._description = _translate("Checker", "Check if the network graph is valid") + self._name = _translate( + "Checker", f"Mage network graph {mode} checker") + self._description = _translate( + "Checker", "Check if the network graph is valid") @timer def _connectivity(self, summary, status, graph): diff --git a/src/Checker/Study.py b/src/Checker/Study.py index 868ac104..b5287e6f 100644 --- a/src/Checker/Study.py +++ b/src/Checker/Study.py @@ -30,7 +30,8 @@ class StudyNetworkReachChecker(AbstractModelChecker): super(StudyNetworkReachChecker, self).__init__() self._name = _translate("Checker", "Study network reach checker") - self._description = _translate("Checker", "Check if exists at least one reach for study") + self._description = _translate( + "Checker", "Check if exists at least one reach for study") def run(self, study): if study is None: @@ -54,12 +55,14 @@ class StudyNetworkReachChecker(AbstractModelChecker): self._status = STATUS.OK return True + class StudyGeometryChecker(AbstractModelChecker): def __init__(self): super(StudyGeometryChecker, self).__init__() self._name = _translate("Checker", "Study geometry checker") - self._description = _translate("Checker", "Check if exists geometry for each reach of study") + self._description = _translate( + "Checker", "Check if exists geometry for each reach of study") self._reachs = [] def run(self, study): @@ -128,6 +131,7 @@ class DummyWARNING(AbstractModelChecker): return True + class DummyERROR(AbstractModelChecker): def __init__(self): super(DummyERROR, self).__init__() diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index d90c6478..5a955b37 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -25,12 +25,13 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class BoundaryCondition(SQLSubModel): _sub_classes = [] _id_cnt = 0 - def __init__(self, id:int = -1, name:str = "", - status = None): + def __init__(self, id: int = -1, name: str = "", + status=None): super(BoundaryCondition, self).__init__() self._status = status @@ -50,7 +51,7 @@ class BoundaryCondition(SQLSubModel): BoundaryCondition._id_cnt = max(BoundaryCondition._id_cnt + 1, self.id) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE boundary_condition( id INTEGER NOT NULL PRIMARY KEY, @@ -76,7 +77,7 @@ class BoundaryCondition(SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return True @classmethod @@ -101,7 +102,7 @@ class BoundaryCondition(SQLSubModel): return res @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): new = [] tab = data["tab"] @@ -115,9 +116,9 @@ class BoundaryCondition(SQLSubModel): t = row[2] ctor = cls._get_ctor_from_type(t) bc = ctor( - id = row[0], - name = row[1], - status = data['status'] + id=row[0], + name=row[1], + status=data['status'] ) bc.node = None @@ -143,7 +144,7 @@ class BoundaryCondition(SQLSubModel): return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): tab = data["tab"] execute(f"DELETE FROM boundary_condition WHERE id = {self.id}") @@ -155,22 +156,22 @@ class BoundaryCondition(SQLSubModel): sql = ( "INSERT INTO " + - "boundary_condition(id, name, type, tab, node) "+ + "boundary_condition(id, name, type, tab, node) " + "VALUES (" + - f"{self.id}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', '{tab}', {node}" + + f"{self.id}, '{self._db_format(self._name)}', " + + f"'{self._db_format(self._type)}', '{tab}', {node}" + ")" ) execute(sql) ind = 0 for d in self._data: - data0 = self._sql_format(str(d[0])) - data1 = self._sql_format(str(d[1])) + data0 = self._db_format(str(d[0])) + data1 = self._db_format(str(d[1])) sql = ( "INSERT INTO " + - "boundary_condition_data(ind, data0, data1, bc) "+ + "boundary_condition_data(ind, data0, data1, bc) " + f"VALUES ({ind}, '{data0}', {data1}, {self.id})" ) execute(sql) @@ -187,8 +188,13 @@ class BoundaryCondition(SQLSubModel): @classmethod def time_convert(cls, data): - if type(data) == str and data.count(":") == 3: - return old_pamhyr_date_to_timestamp(data) + if type(data) is str: + if data.count(":") == 3: + return old_pamhyr_date_to_timestamp(data) + if data.count(":") == 2: + return old_pamhyr_date_to_timestamp("00:" + data) + if data.count(".") == 1: + return round(float(data)) return int(data) @@ -246,7 +252,7 @@ class BoundaryCondition(SQLSubModel): new_1 = self._default_1 if len(header) != 0: - for i in [0,1]: + for i in [0, 1]: for j in range(len(header)): if self._header[i] == header[j]: if i == 0: @@ -259,13 +265,13 @@ class BoundaryCondition(SQLSubModel): return (new_0, new_1) - def add(self, index:int): + def add(self, index: int): value = (self._default_0, self._default_1) self._data.insert(index, value) self._status.modified() return value - def insert(self, index:int, value): + def insert(self, index: int, value): self._data.insert(index, value) self._status.modified() @@ -301,11 +307,10 @@ class BoundaryCondition(SQLSubModel): return self.data[index] def get_range(self, _range): - l = [] + lst = [] for r in _range: - l.append(r) - return l - + lst.append(r) + return lst def _set_i_c_v(self, index, column, value): v = list(self._data[index]) @@ -313,22 +318,22 @@ class BoundaryCondition(SQLSubModel): self._data[index] = tuple(v) self._status.modified() - def set_i_0(self, index:int, value): + def set_i_0(self, index: int, value): self._set_i_c_v(index, 0, value) - def set_i_1(self, index:int, value): + def set_i_1(self, index: int, value): self._set_i_c_v(index, 1, value) @timer def convert(self, cls): - new = cls(name = self.name, status = self._status) + new = cls(name=self.name, status=self._status) new.node = self.node for i, _ in enumerate(self.data): new.add(i) - for i in [0,1]: - for j in [0,1]: + for i in [0, 1]: + for j in [0, 1]: if self._header[i] == new.header[j]: for ind, v in self.data: try: diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index eb612d9f..2ff27885 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -30,6 +30,7 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( Solid, ) + class BoundaryConditionList(PamhyrModelListWithTab): _tabs_list = ["liquid", "solid", "suspenssion"] _sub_classes = [ @@ -37,21 +38,21 @@ class BoundaryConditionList(PamhyrModelListWithTab): ] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data['status']) + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) if data is None: data = {} for tab in new._tabs: data["tab"] = tab - new._tabs[tab] = BoundaryCondition._sql_load( + new._tabs[tab] = BoundaryCondition._db_load( execute, data ) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): execute("DELETE FROM boundary_condition") if data is None: @@ -60,7 +61,7 @@ class BoundaryConditionList(PamhyrModelListWithTab): for tab in self._tabs: data["tab"] = tab for bc in self._tabs[tab]: - bc._sql_save(execute, data = data) + bc._db_save(execute, data=data) return True @@ -73,18 +74,31 @@ class BoundaryConditionList(PamhyrModelListWithTab): def __copy__(self): new = BoundaryConditionList() - for l in self._tabs: - new.tabs[l] = self._tabs[l].copy() + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].copy() return new def __deepcopy__(self): new = BoundaryConditionList() - for l in self._tabs: - new.tabs[l] = self._tabs[l].deepcopy() + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].deepcopy() return new def copy(self): return copy(self) + + def get_assoc_to_node(self, tab, node): + assoc = list( + filter( + lambda i: i.node is node, + self._tabs[tab] + ) + ) + + if len(assoc) > 0: + return assoc[0] + + return None diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index af3c91fd..176bb788 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -22,7 +22,7 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition class NotDefined(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(NotDefined, self).__init__(id=id, name=name, status=status) self._type = "ND" @@ -32,9 +32,11 @@ class NotDefined(BoundaryCondition): def _default_0(self): return 0.0 + class PonctualContribution(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): - super(PonctualContribution, self).__init__(id=id, name=name, status=status) + def __init__(self, id: int = -1, name: str = "", status=None): + super(PonctualContribution, self).__init__( + id=id, name=name, status=status) self._type = "PC" self._header = ["time", "discharge"] @@ -44,8 +46,9 @@ class PonctualContribution(BoundaryCondition): def compatibility(cls): return ["liquid"] + class TimeOverZ(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(TimeOverZ, self).__init__(id=id, name=name, status=status) self._type = "TZ" @@ -56,9 +59,11 @@ class TimeOverZ(BoundaryCondition): def compatibility(cls): return ["liquid"] + class TimeOverDischarge(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): - super(TimeOverDischarge, self).__init__(id=id, name=name, status=status) + def __init__(self, id: int = -1, name: str = "", status=None): + super(TimeOverDischarge, self).__init__( + id=id, name=name, status=status) self._type = "TD" self._header = ["time", "discharge"] @@ -68,8 +73,9 @@ class TimeOverDischarge(BoundaryCondition): def compatibility(cls): return ["liquid"] + class ZOverDischarge(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(ZOverDischarge, self).__init__(id=id, name=name, status=status) self._type = "ZD" @@ -86,7 +92,7 @@ class ZOverDischarge(BoundaryCondition): class Solid(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(Solid, self).__init__(id=id, name=name, status=status) self.d50 = 0.002 diff --git a/src/Model/Except.py b/src/Model/Except.py index 7d5e3959..ef45ffd0 100644 --- a/src/Model/Except.py +++ b/src/Model/Except.py @@ -45,8 +45,9 @@ def exception_message_box(exception): # Custom error # ################ + class ExeceptionWithMessageBox(Exception): - def __init__(self, title = "Exeception"): + def __init__(self, title="Exeception"): self.title = title def header(self): @@ -72,7 +73,7 @@ class ExeceptionWithMessageBox(Exception): class NotImplementedMethodeError(ExeceptionWithMessageBox): def __init__(self, obj, func): super(NotImplementedMethodeError, self).__init__( - title = _translate("Exception", "Method not implemented") + title=_translate("Exception", "Method not implemented") ) self.obj = obj @@ -81,12 +82,16 @@ class NotImplementedMethodeError(ExeceptionWithMessageBox): self.alert() def __str__(self): + class_type = self.obj + if self.obj.__class__ != type: + class_type = self.obj.__class__ + return ( _translate("Exception", "Method") + f" '{self.func.__name__}' " + _translate("Exception", "not implemented") + _translate("Exception", "for class") + - f" '{self.obj.__class__ if self.obj.__class__ != type else self.obj}'" + f" '{class_type}'" ) def header(self): @@ -104,10 +109,11 @@ class NotImplementedMethodeError(ExeceptionWithMessageBox): f" '{self.obj.__class__}'" ) + class FileFormatError(ExeceptionWithMessageBox): def __init__(self, filename, reason): super(FileFormatError, self).__init__( - title = _translate("Exception", "FileFormatError") + title=_translate("Exception", "FileFormatError") ) self.reason = reason @@ -137,7 +143,7 @@ class FileFormatError(ExeceptionWithMessageBox): class ClipboardFormatError(ExeceptionWithMessageBox): def __init__(self, mime=None, header=None, data=None): super(ClipboardFormatError, self).__init__( - title = _translate("Exception", "Clipboard format error") + title=_translate("Exception", "Clipboard format error") ) self._mime = mime diff --git a/src/Model/Friction/Friction.py b/src/Model/Friction/Friction.py index 2ebd2b4e..d3be3cf5 100644 --- a/src/Model/Friction/Friction.py +++ b/src/Model/Friction/Friction.py @@ -16,12 +16,17 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel +logger = logging.getLogger() + + class Friction(SQLSubModel): - def __init__(self, name:str = "", status = None): + def __init__(self, name: str = "", status=None): super(Friction, self).__init__() self._status = status @@ -34,7 +39,7 @@ class Friction(SQLSubModel): self._end_strickler = None @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE friction( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -53,13 +58,14 @@ class Friction(SQLSubModel): return True @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return True @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): new = [] - reach = data["parent"] # Reach object + + reach = data["reach"] status = data["status"] stricklers = data["stricklers"].stricklers @@ -68,9 +74,6 @@ class Friction(SQLSubModel): f"FROM friction WHERE reach = {reach.id}" ) - for _ in table: - new.append(None) - for row in table: ind = row[0] # Get stricklers @@ -78,22 +81,23 @@ class Friction(SQLSubModel): es = next(filter(lambda s: s.id == row[4], stricklers)) # Create friction - sec = cls(status = status) + sec = cls(status=status) sec.edge = reach sec.begin_kp = row[1] sec.end_kp = row[2] sec.begin_strickler = bs sec.end_strickler = es - new[ind] = sec + new.append((ind, sec)) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): ind = data["ind"] execute( "INSERT INTO " + - "friction(ind, begin_kp, end_kp, reach, begin_strickler, end_strickler) " + + "friction(ind, begin_kp, end_kp, " + + "reach, begin_strickler, end_strickler) " + "VALUES (" + f"{ind}, {self._begin_kp}, {self._end_kp}, " + f"{self._edge.id}, " + @@ -116,12 +120,16 @@ class Friction(SQLSubModel): def edge(self): return self._edge + @property + def reach(self): + return self._edge + @edge.setter def edge(self, edge): self._edge = edge if (edge is not None and self._begin_kp == 0.0 and - self._end_kp == 0.0): + self._end_kp == 0.0): self._begin_kp = self._edge.reach.get_kp_min() self._end_kp = self._edge.reach.get_kp_max() self._status.modified() @@ -129,6 +137,15 @@ class Friction(SQLSubModel): def has_edge(self): return self._edge is not None + def has_coefficient(self): + return ( + self._begin_strickler is not None and + self._end_strickler is not None + ) + + def is_full_defined(self): + return self.has_edge() and self.has_coefficient() + @property def begin_kp(self): return self._begin_kp diff --git a/src/Model/Friction/FrictionList.py b/src/Model/Friction/FrictionList.py index 215f96dd..6a5a6c09 100644 --- a/src/Model/Friction/FrictionList.py +++ b/src/Model/Friction/FrictionList.py @@ -28,52 +28,77 @@ from Model.Friction.Friction import Friction logger = logging.getLogger() + class FrictionList(PamhyrModelList): _sub_classes = [ Friction ] @classmethod - def _sql_update_0_0_1(cls, execute, version): + def _db_update_0_0_1(cls, execute, version): execute("ALTER TABLE `section` RENAME TO `friction`") @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): if version == "0.0.0": logger.info(f"Update friction TABLE from {version}") - cls._sql_update_0_0_1(execute, version) + cls._db_update_0_0_1(execute, version) return True @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data['status']) + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) - new._lst = Friction._sql_load( + ilst = Friction._db_load( execute, data ) + new._lst = list(map(lambda x: x[1], sorted(ilst))) + return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): + frictions = self.lst + reach = data["reach"] execute(f"DELETE FROM friction WHERE reach = {reach.id}") ok = True ind = 0 - for friction in self._lst: + for friction in frictions: data["ind"] = ind - ok &= friction._sql_save(execute, data = data) + ok &= friction._db_save(execute, data=data) ind += 1 return ok + def _get_frictions_list(self): + # Frictions list generator is type (int, Point) with the first + # element the index of the Point in list + return list( + map( + lambda p: p[1], + sorted( + self._lst, + key=lambda p: p[0] + ) + ) + ) + + @property + def lst(self): + if not isinstance(self._lst, list): + self._lst = self._get_frictions_list() + + return self._lst + @property def frictions(self): return self.lst def new(self, index): - n = Friction(status = self._status) + n = Friction(status=self._status) self._lst.insert(index, n) self._status.modified() return n diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index cc1971c0..d6deead8 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -18,8 +18,9 @@ from Model.Except import NotImplementedMethodeError + class Point(object): - def __init__(self, name:str = "", profile=None, status = None): + def __init__(self, name: str = "", profile=None, status=None): super(Point, self).__init__() self._status = status @@ -28,7 +29,6 @@ class Point(object): self._profile = profile self._sl = None - @property def sl(self): if self._sl is None: @@ -40,8 +40,8 @@ class Point(object): def sl(self, sl): if sl == self._profile.sl: self._sl = None - - self._sl = sl + else: + self._sl = sl @property def name(self): diff --git a/src/Model/Geometry/PointXY.py b/src/Model/Geometry/PointXY.py index 7448d045..3c958721 100644 --- a/src/Model/Geometry/PointXY.py +++ b/src/Model/Geometry/PointXY.py @@ -20,10 +20,11 @@ from math import dist from Model.Geometry.Point import Point + class PointAC(Point): - def __init__(self, a:float = 0.0, c:float = 0.0, - name: str = "", status = None): - super(PointXY, self).__init__(name = name, status = status) + def __init__(self, a: float = 0.0, c: float = 0.0, + name: str = "", status=None): + super(PointXY, self).__init__(name=name, status=status) self._a = float(a) self._c = float(c) diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 95f44219..7da7903e 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -22,19 +22,21 @@ import numpy as np from Model.Tools.PamhyrDB import SQLSubModel from Model.Geometry.Point import Point + class PointXYZ(Point, SQLSubModel): _sub_classes = [] - def __init__(self, x:float = 0.0, y:float = 0.0, z:float = 0.0, - name:str = "", profile = None, status = None): - super(PointXYZ, self).__init__(name=name, profile=profile, status=status) + def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, + name: str = "", profile=None, status=None): + super(PointXYZ, self).__init__( + name=name, profile=profile, status=status) self._x = float(x) self._y = float(y) self._z = float(z) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE geometry_pointXYZ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -53,7 +55,7 @@ class PointXYZ(Point, SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): cls._update_submodel(execute, version) major, minor, release = version.strip().split(".") @@ -69,9 +71,7 @@ class PointXYZ(Point, SQLSubModel): return True @classmethod - def _sql_load(cls, execute, data = None): - points = [] - + def _db_load(cls, execute, data=None): status = data["status"] profile = data["profile"] @@ -81,10 +81,6 @@ class PointXYZ(Point, SQLSubModel): f"WHERE profile = {profile.id}" ) - # Create points list - for _ in table: - points.append(None) - # Fill points list with new point for row in table: ind = row[0] @@ -95,13 +91,13 @@ class PointXYZ(Point, SQLSubModel): sl = row[5] new = cls( - name = name, - x = x, y = y, z = z, - profile = profile, - status = status + name=name, + x=x, y=y, z=z, + profile=profile, + status=status ) - if sl == -1 or sl == None: + if sl == -1 or sl is None: new._sl = None else: new._sl = next( @@ -111,21 +107,19 @@ class PointXYZ(Point, SQLSubModel): ) ) - points[ind] = new + yield ind, new - return points - - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): profile = data["profile"] ind = data["ind"] sl = self._sl.id if self._sl is not None else -1 sql = ( - "INSERT OR REPLACE INTO " + - "geometry_pointXYZ(ind, name, x, y, z, profile, sl) "+ + "INSERT INTO " + + "geometry_pointXYZ(ind, name, x, y, z, profile, sl) " + "VALUES (" + - f"{ind}, '{self._sql_format(self._name)}', " + + f"{ind}, '{self._db_format(self._name)}', " + f"{self.x}, {self.y}, {self.z}, " + f"{profile.id}, {sl}" + ")" @@ -134,7 +128,6 @@ class PointXYZ(Point, SQLSubModel): return True - @classmethod def from_data(cls, header, data): point = None @@ -157,7 +150,6 @@ class PointXYZ(Point, SQLSubModel): return point - def __repr__(self): return f"({self._x}, {self._y}, {self._z}, {self._name})" diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 4e72ca64..a510b308 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -25,14 +25,15 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class Profile(object): _id_cnt = 0 - def __init__(self, id:int = -1, num:int = 0, - kp:float = 0.0, name:str = "", - code1:int = 0, code2:int = 0, - _type:str = "", reach = None, - status = None): + def __init__(self, id: int = -1, num: int = 0, + kp: float = 0.0, name: str = "", + code1: int = 0, code2: int = 0, + _type: str = "", reach=None, + status=None): super(Profile, self).__init__() self._status = status @@ -56,16 +57,35 @@ class Profile(object): self._profile_type = _type + def __len__(self): + return len(self.points) + @property def number_points(self): - return len(self._points) + return len(self.points) + + def _get_points_list(self): + # Points list generator is type (int, Point) with the first + # element the index of the Point in list + return list( + map( + lambda p: p[1], + sorted( + self._points, + key=lambda p: p[0] + ) + ) + ) @property def points(self): - return self._points.copy() + if not isinstance(self._points, list): + self._points = self._get_points_list() + + return self._points def point(self, index): - return self._points[index] + return self.points[index] @property def reach(self): @@ -112,7 +132,7 @@ class Profile(object): @property def nb_points(self): - return len(self._points) + return len(self.points) @property def kp(self): @@ -166,9 +186,9 @@ class Profile(object): self._profile_type = value self._status.modified() - def point(self, i:int): - if i < len(self._points): - return self._points[i] + def point(self, i: int): + if i < len(self.points): + return self.points[i] return None @@ -178,11 +198,10 @@ class Profile(object): Returns: The list of named point """ - return [point for point in self._points + return [point for point in self.points if point.point_is_named()] - - def insert_point(self, index: int, point:Point): + def insert_point(self, index: int, point: Point): """Insert point at index. Args: @@ -192,7 +211,7 @@ class Profile(object): Returns: Nothing. """ - self._points.insert(index, point) + self.points.insert(index, point) self._status.modified() @@ -209,31 +228,17 @@ class Profile(object): self._status.modified() - def delete(self, indexes: int): - """Delete points at index - - Args: - indexes: List of index of points. - - Returns: - Nothing. - """ - points = set( + def delete_i(self, indexes: list): + self._points = list( map( lambda e: e[1], filter( - lambda e: e[0] in indexes, + lambda e: e[0] not in indexes, enumerate(self.points) ) ) ) - self._points = list( - filter( - lambda p: p not in points, - self.points - ) - ) self._status.modified() def delete_points(self, points): @@ -256,10 +261,10 @@ class Profile(object): # Move def move_up_point(self, index: int): - if index < len(self._points): + if index < len(self.points): next = index - 1 - p = self._points + p = self.points p[index], p[next] = p[next], p[index] self._status.modified() @@ -267,7 +272,7 @@ class Profile(object): if index >= 0: prev = index + 1 - p = self._points + p = self.points p[index], p[prev] = p[prev], p[index] self._status.modified() @@ -275,14 +280,14 @@ class Profile(object): @timer def sort(self, column, is_reversed: bool = False): - predicate = lambda p: p.x + def predicate(p): return p.x if column == 'y': - predicate = lambda p: p.y + def predicate(p): return p.y elif column == 'z': - predicate = lambda p: p.z + def predicate(p): return p.z self._points = sorted( - self._points, + self.points, key=predicate, reverse=is_reversed ) @@ -290,7 +295,7 @@ class Profile(object): @timer def sort_with_indexes(self, indexes: list): - if len(self._points) != len(indexes): + if len(self.points) != len(indexes): logger.critical("Indexes list do not correspond to point list") self._points = list( @@ -340,3 +345,13 @@ class Profile(object): # Abstract method, must be implemented for in non abstract class def get_station(self): raise NotImplementedMethodeError(self, self.get_station) + + # Computation method + + # Abstract method for width approximation + def width_approximation(self): + raise NotImplementedMethodeError(self, self.width_approximation) + + # Abstract method for width approximation + def get_water_limits(self, z): + raise NotImplementedMethodeError(self, self.get_water_limits) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index da1ebdfb..92a1a0d3 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -16,6 +16,7 @@ # -*- coding: utf-8 -*- +import logging import numpy as np from typing import List @@ -27,6 +28,9 @@ from Model.Geometry.Profile import Profile from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.Vector_1d import Vector1d +logger = logging.getLogger() + + class ProfileXYZ(Profile, SQLSubModel): _sub_classes = [ PointXYZ, @@ -36,11 +40,11 @@ class ProfileXYZ(Profile, SQLSubModel): id: int = -1, name: str = "", kp: float = 0., - reach = None, - num = 0, + reach=None, + num=0, nb_point: int = 0, code1: int = 0, code2: int = 0, - status = None): + status=None): """ProfileXYZ constructor Args: @@ -54,18 +58,18 @@ class ProfileXYZ(Profile, SQLSubModel): Nothing. """ super(ProfileXYZ, self).__init__( - id = id, - num = num, - name = name, - kp = kp, - code1 = code1, code2 = code2, - _type = "XYZ", - reach = reach, - status = status, + id=id, + num=num, + name=name, + kp=kp, + code1=code1, code2=code2, + _type="XYZ", + reach=reach, + status=status, ) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE geometry_profileXYZ( id INTEGER NOT NULL PRIMARY KEY, @@ -85,7 +89,7 @@ class ProfileXYZ(Profile, SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): major, minor, release = version.strip().split(".") if major == minor == "0": if int(release) < 2: @@ -100,7 +104,7 @@ class ProfileXYZ(Profile, SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): profiles = [] status = data["status"] reach = data["reach"] @@ -108,12 +112,9 @@ class ProfileXYZ(Profile, SQLSubModel): table = execute( "SELECT id, ind, name, kp, num, code1, code2, sl " + "FROM geometry_profileXYZ " + - f"WHERE reach = {reach}" + f"WHERE reach = {reach.id}" ) - for _ in table: - profiles.append(None) - for row in table: id = row[0] ind = row[1] @@ -125,14 +126,14 @@ class ProfileXYZ(Profile, SQLSubModel): sl = row[7] new = cls( - id=id, num = num, - name = name, kp = kp, - code1 = code1, code2 = code2, - reach = data["parent"], - status = status + id=id, num=num, + name=name, kp=kp, + code1=code1, code2=code2, + reach=reach, + status=status ) - if sl == -1 or sl == None: + if sl == -1 or sl is None: new._sl = None else: new._sl = next( @@ -143,13 +144,11 @@ class ProfileXYZ(Profile, SQLSubModel): ) data["profile"] = new - new._points = PointXYZ._sql_load(execute, data) + new._points = PointXYZ._db_load(execute, data.copy()) - profiles[ind] = new + yield ind, new - return profiles - - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): ok = True ind = data["ind"] @@ -157,22 +156,25 @@ class ProfileXYZ(Profile, SQLSubModel): sql = ( "INSERT OR REPLACE INTO " + - "geometry_profileXYZ(id, ind, name, reach, kp, num, code1, code2, sl) "+ + "geometry_profileXYZ(id, ind, name, reach, " + + "kp, num, code1, code2, sl) " + "VALUES (" + - f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + + f"{self.id}, {ind}, '{self._db_format(self._name)}', " + f"{self.reach.id}, {self.kp}, {self.num}, " + f"{self.code1}, {self.code1}, {sl}" + ")" ) execute(sql) + points = self.points + data["profile"] = self execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}") ind = 0 - for point in self._points: + for point in points: data["ind"] = ind - ok &= point._sql_save(execute, data) + ok &= point._db_save(execute, data) ind += 1 return ok @@ -182,11 +184,20 @@ class ProfileXYZ(Profile, SQLSubModel): profile = None try: if len(header) == 0: + name = data[0] + kp = data[1] + reach = data[2] + status = data[3] + profile = cls( - *data + id=-1, + name=name, + kp=kp, + reach=reach, + status=status ) else: - valid_header = {'name', 'reach', 'kp'} + valid_header = {'name', 'reach', 'kp', 'status'} d = {} for i, v in enumerate(data): h = header[i].strip().lower().split(' ')[0] @@ -195,6 +206,7 @@ class ProfileXYZ(Profile, SQLSubModel): profile = cls(**d) except Exception as e: + logger.error(e) raise ClipboardFormatError(header, data) return profile @@ -220,18 +232,17 @@ class ProfileXYZ(Profile, SQLSubModel): return point - def x(self): - return [point.x for point in self._points] + return [point.x for point in self.points] def y(self): - return [point.y for point in self._points] + return [point.y for point in self.points] def z(self): - return [point.z for point in self._points] + return [point.z for point in self.points] def names(self): - return [point.name for point in self._points] + return [point.name for point in self.points] def x_max(self): return max(self.filter_isnan(self.x())) @@ -262,7 +273,7 @@ class ProfileXYZ(Profile, SQLSubModel): """ for point in list_points: pt = PointXYZ(*point, profile=self, status=self._status) - self._points.append(pt) + self.points.append(pt) self._status.modified() def get_point_i(self, index: int) -> PointXYZ: @@ -275,10 +286,31 @@ class ProfileXYZ(Profile, SQLSubModel): The point. """ try: - return self._points[index] + return self.points[index] except IndexError: raise IndexError(f"Invalid point index: {index}") + def get_point_by_name(self, name: str) -> PointXYZ: + """Get point by name. + + Args: + name: Point name. + + Returns: + The point. + """ + try: + n_name = name.lower().strip() + return next( + filter( + lambda p: p.name.lower().strip() == n_name, + self.points + ) + ) + except Exception as e: + logger.debug(f"{e}") + raise IndexError(f"Invalid point name: {name}") + def add(self): """Add a new PointXYZ to profile. @@ -286,7 +318,7 @@ class ProfileXYZ(Profile, SQLSubModel): Nothing. """ point_xyz = PointXYZ(0., 0., 0., profile=self, status=self._status) - self._points.append(point_xyz) + self.points.append(point_xyz) self._status.modified() def insert(self, index: int): @@ -299,7 +331,7 @@ class ProfileXYZ(Profile, SQLSubModel): The new point. """ point = PointXYZ(0., 0., 0., profile=self, status=self._status) - self._points.insert(index, point) + self.points.insert(index, point) self._status.modified() return point @@ -315,9 +347,9 @@ class ProfileXYZ(Profile, SQLSubModel): return [x for x in lst if not np.isnan(x)] def _first_point_not_nan(self): - first_point = self._points[0] + first_point = self.points[0] - for point in self._points: + for point in self.points: if not point.is_nan(): first_point = point break @@ -325,9 +357,9 @@ class ProfileXYZ(Profile, SQLSubModel): return first_point def _last_point_not_nan(self): - last_point = self._points[-1] + last_point = self.points[-1] - for point in self._points[::-1]: + for point in self.points[::-1]: if not point.is_nan(): last_point = point break @@ -354,13 +386,13 @@ class ProfileXYZ(Profile, SQLSubModel): first_point_not_nan = self._first_point_not_nan() last_point_not_nan = self._last_point_not_nan() - for index, point in enumerate(self._points): + for index, point in enumerate(self.points): if point.point_is_named(): index_first_named_point = index first_named_point = point break - for point in reversed(self._points): + for point in reversed(self.points): if point.point_is_named(): last_named_point = point break @@ -369,32 +401,32 @@ class ProfileXYZ(Profile, SQLSubModel): constant = 0.0 if (first_named_point is not None and - last_named_point is not None): + last_named_point is not None): if (first_named_point != last_named_point and - first_named_point.x != last_named_point.x): + first_named_point.x != last_named_point.x): vector = Vector1d(first_named_point, last_named_point) - normalized_direction_vec = vector.normalized_direction_vector() + norm_dir_vec = vector.normalized_direction_vector() else: vector = Vector1d(first_point_not_nan, last_point_not_nan) - normalized_direction_vec = vector.normalized_direction_vector() + norm_dir_vec = vector.normalized_direction_vector() - for point in self._points: + for point in self.points: xi = point.x - first_named_point.x yi = point.y - first_named_point.y - station_i = (normalized_direction_vec[0] * xi + - normalized_direction_vec[1] * yi) + station_i = (norm_dir_vec[0] * xi + + norm_dir_vec[1] * yi) station.append(station_i) constant = station[index_first_named_point] elif first_named_point is None: vector = Vector1d(first_point_not_nan, last_point_not_nan) - normalized_direction_vec = vector.normalized_direction_vector() + norm_dir_vec = vector.normalized_direction_vector() - for point in self._points: + for point in self.points: xi = point.x - first_point_not_nan.x yi = point.y - first_point_not_nan.y - station_i = (normalized_direction_vec[0] * xi + - normalized_direction_vec[1] * yi) + station_i = (norm_dir_vec[0] * xi + + norm_dir_vec[1] * yi) station.append(station_i) z_min = self.z_min() @@ -457,7 +489,7 @@ class ProfileXYZ(Profile, SQLSubModel): if (self.number_points <= np): return points while len(points) > np: l2 = [] - for i in range(len(points)-2) + for i in range(len(points)-2): l2.append(PointXYZ.angle(points(i),points(i+1),points(i+2))) imax = np.argmax(l2)+1 points.pop(imax) @@ -471,7 +503,7 @@ class ProfileXYZ(Profile, SQLSubModel): if (self.number_points <= np): return points while len(points) > np: l2 = [] - for i in range(len(points)-2) + for i in range(len(points)-2): l2.append(PointXYZ.triangle_area(points(i),points(i+1),points(i+2))) imin = np.argmin(l2)+1 points.pop(imin) @@ -479,5 +511,64 @@ class ProfileXYZ(Profile, SQLSubModel): return points + def width_approximation(self): + rg = self.get_point_by_name("rg") + rd = self.get_point_by_name("rd") + return abs(rg.dist(rd)) + def get_water_limits(self, z): + # ==================================================================== + # détermination des points limites RG et RD pour un niveau + # d'eau donné + # + # irg et ird sont les premiers indices en partant des rives + # gauche et droite qui correspondent à des points sous la + # surface de l'eau ptX et ptY sont les points interpolés où + # le plan d'eau intersecte le profil known_level est le + # niveau d'eau pour lequel on a obtenu irg, ird, ptX et ptY + # ==================================================================== + + # initialisation + irg = -1 + ird = -1 + + for i in range(self.number_points): + if self.point(i).z <= z: + irg = i + + for i in reversed(range(self.number_points)): + if self.point(i).z <= z: + ird = i + + # interpolation des points ptX et ptY + if (irg < self.number_points - 1): + x = np.interp( + z, + [self.point(irg).z, self.point(irg + 1).z], + [self.point(irg).x, self.point(irg + 1).x] + ) + y = np.interp( + z, + [self.point(irg).z, self.point(irg + 1).z], + [self.point(irg).y, self.point(irg + 1).y] + ) + ptX = PointXYZ(x, y, z) + else: + ptX = self.point(0) + if (ird > 0): + x = np.interp( + z, + [self.point(ird-1).z, self.point(ird).z], + [self.point(ird-1).x, self.point(ird).x] + ) + y = np.interp( + z, + [self.point(ird).z, self.point(ird - 1).z], + [self.point(ird).y, self.point(ird - 1).y] + ) + ptY = PointXYZ(x, y, z) + else: + ptY = self.point(self.number_points - 1) + + return ptX, ptY diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index dd7e8138..ee594e19 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -36,13 +36,14 @@ from Model.Except import FileFormatError, exception_message_box logger = logging.getLogger() + class Reach(SQLSubModel): _sub_classes = [ ProfileXYZ, ] def __init__(self, status=None, parent=None): - self.id = parent.id + self.id = parent.id if parent is not None else 0 self._status = status self._parent = parent self._profiles: List[Profile] = [] @@ -51,36 +52,37 @@ class Reach(SQLSubModel): self._guidelines = {} @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"], parent = data["parent"]) + def _db_load(cls, execute, data=None): + new = cls(status=data["status"], parent=data["reach"]) - new._profiles = ProfileXYZ._sql_load( + new._profiles = ProfileXYZ._db_load( execute, - data = data + data=data ) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): + profiles = self.profiles + # Delete old data execute(f"DELETE FROM geometry_profileXYZ WHERE reach = {self.id}") - # execute(f"DELETE FROM geometry_pointXYZ") if data is None: data = {} ind = 0 - for profile in self._profiles: + for profile in profiles: data["ind"] = ind - profile._sql_save(execute, data) + profile._db_save(execute, data) ind += 1 return True @@ -94,26 +96,53 @@ class Reach(SQLSubModel): Returns: The profile at index i """ - if i < len(self._profiles): - return self._profiles[i] + if i < len(self.profiles): + return self.profiles[i] return None @property def name(self): - if self._parent == None: + if self._parent is None: return "" return self._parent.name + def _get_profiles_list(self): + # Profiles list generator is type (int, Point) with the first + # element the index of the Point in list + logger.info(f"Load profiles from reach {self.name}") + return list( + map( + lambda p: p[1], + sorted( + self._profiles, + key=lambda p: p[0] + ) + ) + ) + + def __len__(self): + if not isinstance(self._profiles, list): + self._profiles = self._get_profiles_list() + + return len(self._profiles) + @property def profiles(self): - return self._profiles.copy() + if not isinstance(self._profiles, list): + self._profiles = self._get_profiles_list() + + return self._profiles + + @profiles.setter + def profiles(self, profiles): + self._profiles = profiles def get_profiles_from_kp(self, kp): return list( filter( - lambda p: p.kp == kp, self._profiles + lambda p: p.kp == kp, self.profiles ) ) @@ -123,14 +152,14 @@ class Reach(SQLSubModel): Returns: Number of profiles """ - return len(self._profiles) + return len(self.profiles) def get_geometry(self) -> List[Profile]: """ Returns: The profiles list. """ - return self._profiles + return self.profiles def _update_profile_numbers(self): """Update profiles index @@ -152,7 +181,7 @@ class Reach(SQLSubModel): """ profile = ProfileXYZ(reach=self, status=self._status) - self._profiles.insert(index, profile) + self.profiles.insert(index, profile) self._update_profile_numbers() self._status.modified() @@ -168,11 +197,10 @@ class Reach(SQLSubModel): Returns: Nothing. """ - self._profiles.insert(index, profile) + self.profiles.insert(index, profile) self._update_profile_numbers() self._status.modified() - def delete(self, indexes): """Delete some elements in profile list @@ -219,12 +247,11 @@ class Reach(SQLSubModel): self._update_profile_numbers() self._status.modified() - def move_up_profile(self, index: int): if index < len(self.profiles): next = index - 1 - p = self._profiles + p = self.profiles p[index], p[next] = p[next], p[index] self._status.modified() @@ -232,7 +259,7 @@ class Reach(SQLSubModel): if index >= 0: prev = index + 1 - p = self._profiles + p = self.profiles p[index], p[prev] = p[prev], p[index] self._status.modified() @@ -275,7 +302,6 @@ class Reach(SQLSubModel): def get_kp_max(self): return max(self.get_kp()) - # Sediment Layers def get_sl(self): @@ -324,12 +350,12 @@ class Reach(SQLSubModel): for guide in guide_set: self._guidelines[guide] = flatten( map( - lambda l: list( + lambda lst: list( # Filter point with name (we assume we have # only one point by profile) filter( lambda p: p.name == guide, - l + lst ) ), named_points @@ -344,7 +370,7 @@ class Reach(SQLSubModel): Tuple of complete and incomplete guidelines name. """ # Get all point contains into a guideline - named_points = [profile.named_points() for profile in self._profiles] + named_points = [profile.named_points() for profile in self.profiles] points_name = list( map( lambda lst: list(map(lambda p: p.name, lst)), @@ -364,7 +390,9 @@ class Reach(SQLSubModel): reduce( lambda acc, h: acc + h, map( - lambda l: list(set(l).symmetric_difference(guide_set)), + lambda lst: list( + set(lst).symmetric_difference(guide_set) + ), points_name ), [] @@ -414,13 +442,12 @@ class Reach(SQLSubModel): def get_guidelines_z(self): return self._map_guidelines_points(lambda p: p.z) - # Sort @timer def sort(self, is_reversed: bool = False): - self._profiles = sorted( - self._profiles, + self.profiles = sorted( + self.profiles, key=lambda profile: profile.kp, reverse=is_reversed ) @@ -428,10 +455,10 @@ class Reach(SQLSubModel): @timer def sort_with_indexes(self, indexes: list): - if len(self._profiles) != len(indexes): + if len(self.profiles) != len(indexes): logger.critical("Indexes list do not correspond to profile list") - self._profiles = list( + self.profiles = list( map( lambda x: x[1], sorted( @@ -449,7 +476,8 @@ class Reach(SQLSubModel): """Import a geometry from file (.ST or .st) Args: - file_path_name: The absolute path of geometry file (.ST or .st) to import. + file_path_name: The absolute path of geometry file (.ST or .st) + to import. Returns: Nothing. @@ -459,7 +487,8 @@ class Reach(SQLSubModel): try: list_profile, list_header = self.read_file_st(str(file_path_name)) - profile_header = ["num", "code1", "code2", "nb_point", "kp", "name"] + profile_header = ["num", "code1", + "code2", "nb_point", "kp", "name"] if list_profile and list_header: for ind, profile in enumerate(list_profile): @@ -471,7 +500,7 @@ class Reach(SQLSubModel): **d, reach=self, status=self._status ) prof.import_points(profile) - self._profiles.append(prof) + self.profiles.append(prof) self._update_profile_numbers() self._status.modified() @@ -535,8 +564,8 @@ class Reach(SQLSubModel): # TODO: Move this function to model reach def export_reach(self, filename): with open(f"{filename}", "w") as file_st: - for index in range(len(self._profiles)): - profile = self._profiles[index] + for index in range(len(self.profiles)): + profile = self.profiles[index] for v in profile.header: file_st.write(f"{v:>6}") @@ -807,3 +836,103 @@ class Reach(SQLSubModel): profiles[-1].import_points(points) return profiles + + def get_incline(self): + first = self.profile(0) + last = self.profile(len(self) - 1) + + z_first = first.z_min() + kp_first = first.kp + + z_last = last.z_min() + kp_last = last.kp + + return ( + (z_last - z_first) + / + (kp_last - kp_first) + ) + + def get_incline_mean(self): + profiles = self.profiles + previous = profiles[0] + + incline_acc = 0 + + for profile in profiles[1:]: + z_first = previous.z_min() + kp_first = previous.kp + + z_last = profile.z_min() + kp_last = profile.kp + + incline_acc += ( + (z_last - z_first) + / + (kp_last - kp_first) + ) + + previous = profile + + return (incline_acc / (len(profiles) - 1)) + + def get_incline_median(self): + profiles = self.profiles + previous = profiles[0] + + incline_acc = [] + + for profile in profiles[1:]: + z_first = previous.z_min() + kp_first = previous.kp + + z_last = profile.z_min() + kp_last = profile.kp + + incline_acc += [ + (z_last - z_first) + / + (kp_last - kp_first) + ] + + previous = profile + + incline_acc.sort() + return incline_acc[round(len(profiles)/2)] + + def get_incline_median_mean(self): + profiles = self.profiles + previous = profiles[0] + + incline_acc = [] + + for profile in profiles[1:]: + z_first = previous.z_min() + kp_first = previous.kp + + z_last = profile.z_min() + kp_last = profile.kp + + incline_acc += [ + (z_last - z_first) + / + (kp_last - kp_first) + ] + + previous = profile + + incline_acc.sort() + + marge = round(len(incline_acc) * 0.1) + incline_set = incline_acc[marge:-marge] + + logger.debug(f"+{incline_acc}") + logger.debug(f"-{incline_set}") + + return ( + reduce( + lambda acc, x: acc + x, + incline_set, + 0.0 + ) / (len(incline_set)) + ) diff --git a/src/Model/Geometry/Vector_1d.py b/src/Model/Geometry/Vector_1d.py index a3da9358..e136f121 100644 --- a/src/Model/Geometry/Vector_1d.py +++ b/src/Model/Geometry/Vector_1d.py @@ -19,6 +19,7 @@ import numpy as np from Model.Geometry.PointXYZ import PointXYZ + class Vector1d: def __init__(self, a: PointXYZ, b: PointXYZ): self.A = a diff --git a/src/Model/HydraulicStructures/Basic/HydraulicStructures.py b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py new file mode 100644 index 00000000..e7a5af5f --- /dev/null +++ b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py @@ -0,0 +1,191 @@ +# HydraulicStructures.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Except import NotImplementedMethodeError + +from Model.HydraulicStructures.Basic.Value import ( + BHSValue +) + +logger = logging.getLogger() + + +class BasicHS(SQLSubModel): + _sub_classes = [ + BHSValue, + ] + _id_cnt = 0 + + def __init__(self, id: int = -1, name: str = "", + status=None): + super(BasicHS, self).__init__() + + self._status = status + + if id == -1: + self.id = BasicHS._id_cnt + else: + self.id = id + + self._name = name + self._type = "" + self._enabled = True + self._data = [] + + BasicHS._id_cnt = max(BasicHS._id_cnt + 1, self.id) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE hydraulic_structures_basic( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + hs INTEGER, + FOREIGN KEY(hs) REFERENCES hydraulic_structures(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _get_ctor_from_type(cls, t): + from Model.HydraulicStructures.Basic.Types import ( + BHS_types, NotDefined, + ) + + res = NotDefined + + if t in BHS_types.keys(): + res = BHS_types[t] + + return res + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, name, type, enabled, hs " + + "FROM hydraulic_structures_basic " + + f"WHERE hs = {data['hs_id']} " + ) + + for row in table: + bhs_id = row[0] + name = row[1] + type = row[2] + enabled = (row[3] == 1) + hs_id = row[4] + + ctor = cls._get_ctor_from_type(type) + bhs = ctor( + id=bhs_id, + name=name, + status=data['status'] + ) + + bhs.enabled = enabled + + data['bhs_id'] = bhs_id + bhs._data = BHSValue._db_load( + execute, data + ) + + new.append(bhs) + + return new + + def _db_save(self, execute, data=None): + hs_id = data['hs_id'] + + sql = ( + "INSERT INTO " + + "hydraulic_structures_basic(id, name, type, enabled, hs) " + + "VALUES (" + + f"{self.id}, " + + f"'{self._db_format(self._name)}', " + + f"'{self._db_format(self._type)}', " + + f"{self._db_format(self.enabled)}, " + + f"{hs_id} " + + ")" + ) + execute(sql) + + data['bhs_id'] = self.id + execute( + "DELETE FROM hydraulic_structures_basic_value " + + f"WHERE bhs = {self.id}" + ) + + for values in self._data: + values._db_save(execute, data) + + return True + + def __len__(self): + return len(self._data) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + self._status.modified() + + @property + def type(self): + return self._type + + @type.setter + def type(self, type): + self._type = type + self._status.modified() + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + self._status.modified() + + @property + def parameters(self): + return self._data + + def convert(self, new_type): + return new_type(id=self.id, name=self.name, status=self._status) diff --git a/src/Model/HydraulicStructures/Basic/Types.py b/src/Model/HydraulicStructures/Basic/Types.py new file mode 100644 index 00000000..1d1d0930 --- /dev/null +++ b/src/Model/HydraulicStructures/Basic/Types.py @@ -0,0 +1,228 @@ +# Types.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from Model.HydraulicStructures.Basic.HydraulicStructures import ( + BasicHS +) + +from Model.HydraulicStructures.Basic.Value import ( + BHSValue +) + + +class NotDefined(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(NotDefined, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "ND" + self._data = [] + + +class SeuilDeversoir(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(SeuilDeversoir, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "S1" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 1.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + ] + + +class SeuilTrapezoidal(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(SeuilTrapezoidal, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "S2" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 1.0, status=status), + BHSValue("Cote de mise en charge", float, 9999.999, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Tangeante du demi angle", float, 0.0, status=status), + ] + + +class SeuilTriangulaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(SeuilTriangulaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "S3" + self._data = [ + BHSValue("Cote", float, 1.0, status=status), + BHSValue("Cote de mise en charge", float, 9999.999, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Tangeante du demi angle", float, 0.0, status=status), + ] + + +class OrificeRectangulaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(OrificeRectangulaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "OR" + self._data = [ + BHSValue("Largeur", float, 0.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("Cote de mise en charge", float, 9999.999, + status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Cote de mise en charge maximale", float, 9999.999, + status=status), + ] + + +class OrificeCirculaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(OrificeCirculaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "OC" + self._data = [ + BHSValue("Diametre", float, 0.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("hauteur envasement", float, 0.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + ] + + +# class OrificeVoute(BasicHS): +# def __init__(self, id: int = -1, name: str = "", +# status=None): +# super(OrificeVoute, self).__init__( +# id=id, name=name, +# status=status +# ) + +# self._type = "OV" +# self._data = [ +# BHSValue("Cote", float, 1.0, status=status), +# BHSValue("Largeur", float, 1.0, status=status), +# BHSValue("Haut de la voute", float, 0.0, status=status), +# BHSValue("Bas de la voute", float, 0.0, status=status), +# BHSValue("Coefficient de debit", float, 0.4, status=status), +# ] + + +class VanneRectangulaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(VanneRectangulaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "V1" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Ouverture", float, 1.0, status=status), + BHSValue("Ouverture maximale", float, 1.0, status=status), + ] + + +class VanneRectangulaireSimplifiee(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(VanneRectangulaireSimplifiee, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "V2" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Ouverture", float, 1.0, status=status), + BHSValue("Ouverture maximale", float, 1.0, status=status), + ] + + +class Borda(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(Borda, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "BO" + self._data = [ + BHSValue("Pas espace", float, 0.1, status=status), + BHSValue("Seuil", float, 0.15, status=status), + BHSValue("Coefficient", float, 0.4, status=status), + ] + + +class UserDefined(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(UserDefined, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "UD" + self._data = [ + BHSValue("Parameter 1", float, 0.0, status=status), + BHSValue("Parameter 2", float, 0.0, status=status), + BHSValue("Parameter 3", float, 0.0, status=status), + BHSValue("Parameter 4", float, 0.0, status=status), + BHSValue("Parameter 5", float, 0.0, status=status), + ] + + +BHS_types = { + "ND": NotDefined, + "S1": SeuilDeversoir, + "S2": SeuilTrapezoidal, + "S3": SeuilTriangulaire, + "OR": OrificeRectangulaire, + "OC": OrificeCirculaire, + # "OV": OrificeVoute, + "V1": VanneRectangulaire, + "V2": VanneRectangulaireSimplifiee, + "BO": Borda, + "UD": UserDefined +} diff --git a/src/Model/HydraulicStructures/Basic/Value.py b/src/Model/HydraulicStructures/Basic/Value.py new file mode 100644 index 00000000..ebf4744e --- /dev/null +++ b/src/Model/HydraulicStructures/Basic/Value.py @@ -0,0 +1,145 @@ +# Value.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from Model.Tools.PamhyrDB import SQLSubModel + + +class BHSValue(SQLSubModel): + _sub_classes = [] + _id_cnt = 0 + + def __init__(self, name: str = "", type=float, value=0.0, + status=None): + super(BHSValue, self).__init__() + + self._status = status + + self._name = name + self._type = type + self._value = type(value) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE hydraulic_structures_basic_value( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + value TEXT NOT NULL, + bhs INTEGER, + FOREIGN KEY(bhs) REFERENCES hydraulic_structures_basic(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _str_to_type(cls, type): + res = str + + if type == "float": + res = float + elif type == "int": + res = int + elif type == "bool": + res = bool + + return res + + @classmethod + def _type_to_str(cls, type): + res = "str" + + if type == float: + res = "float" + elif type == int: + res = "int" + elif type == bool: + res = "bool" + + return res + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + bhs_id = data["bhs_id"] + + table = execute( + "SELECT name, type, value " + + "FROM hydraulic_structures_basic_value " + + f"WHERE bhs = '{bhs_id}'" + ) + + for row in table: + name = row[0] + type = cls._str_to_type(row[1]) + value = row[2] + + val = cls( + name=name, + type=type, + value=value, + status=data['status'] + ) + + new.append(val) + + return new + + def _db_save(self, execute, data=None): + bhs_id = data["bhs_id"] + + sql = ( + "INSERT INTO " + + "hydraulic_structures_basic_value(name, type, value, bhs) " + + "VALUES (" + + f"'{self._db_format(self._name)}', " + + f"'{self._db_format(self._type_to_str(self._type))}', " + + f"'{self._db_format(self._value)}', " + + f"{bhs_id}" + + ")" + ) + execute(sql) + + return True + + @property + def name(self): + return self._name + + @property + def type(self): + return self._type + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._value = self._type(value) + self._status.modified() diff --git a/src/Model/HydraulicStructures/HydraulicStructures.py b/src/Model/HydraulicStructures/HydraulicStructures.py new file mode 100644 index 00000000..c301439b --- /dev/null +++ b/src/Model/HydraulicStructures/HydraulicStructures.py @@ -0,0 +1,285 @@ +# HydraulicStructures.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging +from functools import reduce + +from tools import trace, timer, old_pamhyr_date_to_timestamp + +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Except import NotImplementedMethodeError + +from Model.HydraulicStructures.Basic.HydraulicStructures import BasicHS +from Model.HydraulicStructures.Basic.Types import ( + NotDefined, +) + +logger = logging.getLogger() + + +class HydraulicStructure(SQLSubModel): + _sub_classes = [ + BasicHS, + ] + _id_cnt = 0 + + def __init__(self, id: int = -1, name: str = "", + status=None): + super(HydraulicStructure, self).__init__() + + self._status = status + + if id == -1: + self.id = HydraulicStructure._id_cnt + else: + self.id = id + + self._name = name + self._input_kp = None + self._output_kp = None + self._input_reach = None + self._output_reach = None + self._enabled = True + self._data = [] + + HydraulicStructure._id_cnt = max( + HydraulicStructure._id_cnt + 1, + self.id + ) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE hydraulic_structures( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + input_kp REAL NOT NULL, + output_kp REAL NOT NULL, + input_reach INTEGER, + output_reach INTEGER, + FOREIGN KEY(input_reach) REFERENCES river_reach(id), + FOREIGN KEY(output_reach) REFERENCES river_reach(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, name, enabled, " + + "input_kp, output_kp, " + + "input_reach, output_reach " + + "FROM hydraulic_structures " + ) + + for row in table: + it = iter(row) + + hs_id = next(it) + name = next(it) + enabled = (next(it) == 1) + input_kp = next(it) + output_kp = next(it) + input_reach_id = next(it) + output_reach_id = next(it) + + hs = cls( + id=hs_id, + name=name, + status=data['status'] + ) + + hs.enabled = enabled + hs.input_kp = input_kp if input_kp != -1 else None + hs.output_kp = output_kp if output_kp != -1 else None + + hs.input_reach, hs.output_reach = reduce( + lambda acc, n: ( + n if n.id == input_reach_id else acc[0], + n if n.id == output_reach_id else acc[1] + ), + data["edges"], + [None, None] + ) + + data['hs_id'] = hs_id + hs._data = BasicHS._db_load(execute, data) + + new.append(hs) + + return new + + def _db_save(self, execute, data=None): + execute(f"DELETE FROM hydraulic_structures WHERE id = {self.id}") + + input_reach_id = -1 + if self._input_reach is not None: + input_reach_id = self._input_reach.id + + output_reach_id = -1 + if self._output_reach is not None: + output_reach_id = self._output_reach.id + + input_kp = -1 + if self.input_kp is not None: + input_kp = self.input_kp + + output_kp = -1 + if self.output_kp is not None: + output_kp = self.output_kp + + sql = ( + "INSERT INTO " + + "hydraulic_structures(" + + " id, name, enabled, input_kp, output_kp, " + + " input_reach, output_reach" + + ") " + + "VALUES (" + + f"{self.id}, '{self._db_format(self._name)}', " + + f"{self._db_format(self.enabled)}, " + + f"{input_kp}, {output_kp}, " + + f"{input_reach_id}, {output_reach_id}" + + ")" + ) + execute(sql) + + data['hs_id'] = self.id + execute( + "DELETE FROM hydraulic_structures_basic " + + f"WHERE hs = {self.id}" + ) + + for basic in self._data: + basic._db_save(execute, data) + + return True + + def __len__(self): + return len(self._data) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + self._status.modified() + + @property + def input_kp(self): + return self._input_kp + + @input_kp.setter + def input_kp(self, input_kp): + self._input_kp = input_kp + self._status.modified() + + @property + def output_kp(self): + return self._output_kp + + @output_kp.setter + def output_kp(self, output_kp): + self._output_kp = output_kp + self._status.modified() + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + self._status.modified() + + @property + def input_reach(self): + return self._input_reach + + @input_reach.setter + def input_reach(self, input_reach): + self._input_reach = input_reach + self._status.modified() + + @property + def output_reach(self): + return self._output_reach + + @output_reach.setter + def output_reach(self, output_reach): + self._output_reach = output_reach + self._status.modified() + + @property + def basic_structures(self): + return self._data.copy() + + def basic_structure(self, index: int): + return self._data[index] + + def add(self, index: int): + value = NotDefined(status=self._status) + self._data.insert(index, value) + self._status.modified() + return value + + def insert(self, index: int, value: BasicHS): + self._data.insert(index, value) + self._status.modified() + + def delete_i(self, indexes): + self._data = list( + map( + lambda e: e[1], + filter( + lambda e: e[0] not in indexes, + enumerate(self._data) + ) + ) + ) + self._status.modified() + + def delete(self, els): + self._data = list( + filter( + lambda e: e not in els, + self._data + ) + ) + self._status.modified() + + def sort(self, _reverse=False, key=None): + if key is None: + self._data.sort(reverse=_reverse) + else: + self._data.sort(reverse=_reverse, key=key) + self._status.modified() diff --git a/src/Model/HydraulicStructures/HydraulicStructuresList.py b/src/Model/HydraulicStructures/HydraulicStructuresList.py new file mode 100644 index 00000000..549d8074 --- /dev/null +++ b/src/Model/HydraulicStructures/HydraulicStructuresList.py @@ -0,0 +1,78 @@ +# HydraulicStructuresList.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +from Model.Tools.PamhyrList import PamhyrModelList +from Model.HydraulicStructures.HydraulicStructures import HydraulicStructure + + +class HydraulicStructureList(PamhyrModelList): + _sub_classes = [ + HydraulicStructure, + ] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + if data is None: + data = {} + + new._lst = HydraulicStructure._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute("DELETE FROM hydraulic_structures") + + if data is None: + data = {} + + for hs in self._lst: + hs._db_save(execute, data=data) + + return True + + def new(self, lst, index): + n = HydraulicStructure(status=self._status) + self._lst.insert(index, n) + self._status.modified() + return n + + def __copy__(self): + new = HydraulicStructureList() + + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].copy() + + return new + + def __deepcopy__(self): + new = HydraulicStructureList() + + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].deepcopy() + + return new + + def copy(self): + return copy(self) diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index 2c50d10c..2d88ea68 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -16,18 +16,23 @@ # -*- coding: utf-8 -*- +import logging + from copy import copy, deepcopy from tools import trace, timer from functools import reduce from Model.Tools.PamhyrDB import SQLSubModel +logger = logging.getLogger() + + class Data(SQLSubModel): - def __init__(self, name:str = "", - comment:str = "", reach = None, - kp:float = 0.0, discharge:float = 0.0, - height:float = 0.0, - status = None): + def __init__(self, name: str = "", + comment: str = "", reach=None, + kp: float = 0.0, discharge: float = 0.0, + height: float = 0.0, + status=None): super(Data, self).__init__() self._status = status @@ -51,7 +56,7 @@ class Data(SQLSubModel): self._update_from_discharge() @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE initial_conditions( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -69,11 +74,11 @@ class Data(SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): id = data["reach"].id table = execute( "SELECT ind, name, comment, kp, discharge, height " + @@ -95,28 +100,29 @@ class Data(SQLSubModel): height = row[5] d = cls( - reach = data["reach"], - status = data["status"], - name = name, - comment = comment, - kp = kp, - discharge = discharge, - height = height, + reach=data["reach"], + status=data["status"], + name=name, + comment=comment, + kp=kp, + discharge=discharge, + height=height, ) new[ind] = d return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): ind = data["ind"] execute( "INSERT INTO " + - "initial_conditions(ind, name, comment, kp, discharge, height, reach) " + + "initial_conditions(ind, name, comment, kp, " + + "discharge, height, reach) " + "VALUES (" + - f"{ind}, '{self._sql_format(self.name)}', " + - f"'{self._sql_format(self._comment)}', " + + f"{ind}, '{self._db_format(self.name)}', " + + f"'{self._db_format(self._comment)}', " + f"{self._kp}, {self._discharge}, {self._height}, " + f"{self._reach.id}" + ")" @@ -124,16 +130,15 @@ class Data(SQLSubModel): return True - def copy(self): new = Data( - name = self.name, - comment = self._comment, - kp = self._kp, - discharge = self._discharge, - height = self._height, - reach = self._reach, - status = self._status, + name=self.name, + comment=self._comment, + kp=self._kp, + discharge=self._discharge, + height=self._height, + reach=self._reach, + status=self._status, ) return new @@ -173,7 +178,7 @@ class Data(SQLSubModel): def _update_from_kp(self): min = self._update_get_min() - self._elevation = min - self._height + self._elevation = min + self._height def _update_from_elevation(self): min = self._update_get_min() @@ -216,7 +221,7 @@ class InitialConditions(SQLSubModel): Data ] - def __init__(self, reach = None, status = None): + def __init__(self, reach=None, status=None): super(InitialConditions, self).__init__() self._status = status @@ -225,37 +230,35 @@ class InitialConditions(SQLSubModel): self._data = [] @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): new = cls( - reach = data["reach"], - status = data["status"] + reach=data["reach"], + status=data["status"] ) - new._data = Data._sql_load( + new._data = Data._db_load( execute, - data = data + data=data ) - if new._data is None: - return None + if new._data is not None: + return new - return new - - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): ok = True ind = 0 for d in self._data: data["ind"] = ind - ok &= d._sql_save(execute, data) + ok &= d._db_save(execute, data) ind += 1 return ok @@ -288,7 +291,7 @@ class InitialConditions(SQLSubModel): self._status.modified() def new(self, index): - n = Data(reach = self._reach, status = self._status) + n = Data(reach=self._reach, status=self._status) self._data.insert(index, n) self._status.modified() @@ -321,7 +324,6 @@ class InitialConditions(SQLSubModel): self._data.sort(reverse=reverse, key=key) self._status.modified() - def _data_get(self, key): return list( map( @@ -341,8 +343,8 @@ class InitialConditions(SQLSubModel): def _sort_by_z_and_kp(self, profiles): profiles.sort( - reverse = False, - key = lambda p: p.kp + reverse=False, + key=lambda p: p.kp ) first_z = profiles[0].z() @@ -350,47 +352,102 @@ class InitialConditions(SQLSubModel): if first_z > last_z: profiles.sort( - reverse = True, - key = lambda p: p.kp + reverse=True, + key=lambda p: p.kp ) - def generate_growing_constante_height(self, height:float): + def generate_growing_constante_height(self, height: float): self._data = [] - profiles = self._reach.reach.profiles + profiles = self._reach.reach.profiles.copy() self._sort_by_z_and_kp(profiles) - prev = None - for profile in profiles: - new = Data(reach = self._reach, status = self._status) - new["kp"] = profile.kp + incline = self._reach.reach.get_incline_median_mean() + logger.debug(f"incline = {incline}") - if prev is None: - new["elevation"] = profile.z_min() + height - else: - new["elevation"] = max( - profile.z_min() + height, - prev["elevation"] - ) + previous_elevation = -99999.99 + for profile in profiles: + width = profile.width_approximation() + strickler = 25 + discharge = ( + ((width * 0.8) + * strickler + * (height ** (5/3)) + * (abs(incline) ** (0.5))) + ) + + elevation = max( + profile.z_min() + height, + previous_elevation + ) + + logger.debug(f"({profile.kp}):") + logger.debug(f" width = {width}") + logger.debug(f" strickler = {strickler}") + logger.debug(f" discharge = {discharge}") + + new = Data(reach=self._reach, status=self._status) + new["kp"] = profile.kp + new["discharge"] = discharge + + new["elevation"] = elevation self._data.append(new) - prev = new + previous_elevation = elevation + self._generate_resort_data(profiles) + + def generate_discharge(self, discharge: float): + self._data = [] + + self._generate_height_estimation_from_discharge( + discharge + ) + + def _generate_height_estimation_from_discharge(self, discharge: float): + profiles = self._reach.reach.profiles.copy() + self._sort_by_z_and_kp(profiles) + + previous_elevation = -99999.99 + + incline = self._reach.reach.get_incline_median_mean() + logger.debug(f"incline = {incline}") + + for profile in profiles: + width = profile.width_approximation() + strickler = 25 + height = ( + discharge + / + ((width * 0.8) * strickler * (abs(incline) ** (0.5))) + ) ** (0.6) + + elevation = max( + profile.z_min() + height, + previous_elevation + ) + + logger.debug(f"({profile.kp}):") + logger.debug(f" width = {width}") + logger.debug(f" strickler = {strickler}") + logger.debug(f" hieght = {height}") + + new = Data(reach=self._reach, status=self._status) + new["kp"] = profile.kp + new["discharge"] = discharge + new["elevation"] = elevation + + previous_elevation = elevation + self._data.append(new) + + self._generate_resort_data(profiles) + + def _generate_resort_data(self, profiles): is_reverse = False if profiles[0].kp > profiles[-1].kp: is_reverse = True self._data.sort( - reverse = not is_reverse, - key = lambda d: d['kp'] + reverse=not is_reverse, + key=lambda d: d['kp'] ) - - def generate_discharge(self, discharge:float): - self._new = [] - - for d in self._data: - n = d.copy() - n['discharge'] = discharge - self._new.append(n) - - self._data = self._new diff --git a/src/Model/InitialConditions/InitialConditionsDict.py b/src/Model/InitialConditions/InitialConditionsDict.py index 40fdca49..0cc75293 100644 --- a/src/Model/InitialConditions/InitialConditionsDict.py +++ b/src/Model/InitialConditions/InitialConditionsDict.py @@ -16,27 +16,33 @@ # -*- coding: utf-8 -*- +import types +import logging + from copy import copy from tools import trace, timer from Model.Tools.PamhyrDict import PamhyrModelDict from Model.InitialConditions.InitialConditions import InitialConditions +logger = logging.getLogger() + + class InitialConditionsDict(PamhyrModelDict): _sub_classes = [ InitialConditions, ] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"]) + def _db_load(cls, execute, data=None): + new = cls(status=data["status"]) for reach in data["edges"]: data["reach"] = reach - ic = InitialConditions._sql_load( + ic = InitialConditions._db_load( execute, - data = data + data=data ) if ic is not None: @@ -44,20 +50,40 @@ class InitialConditionsDict(PamhyrModelDict): return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): ok = True if data is None: data = {} - execute("DELETE FROM initial_conditions") - - for reach in self._dict: + ics = self._dict + for reach in ics: data["reach"] = reach - ok &= self._dict[reach]._sql_save(execute, data) + v = self._dict[reach] + if isinstance(v, types.GeneratorType): + self._dict[reach] = list(v)[0] + + execute( + "DELETE FROM initial_conditions " + + f"WHERE reach = '{reach.id}'" + ) + + ok &= self._dict[reach]._db_save(execute, data) return ok + def get(self, key): + if key in self._dict: + v = self._dict[key] + if isinstance(v, types.GeneratorType): + self._dict[key] = list(v)[0] + + return self._dict[key] + + new = self.new(key) + self.set(key, new) + return new + def new(self, reach): - new = InitialConditions(reach = reach, status = self._status) + new = InitialConditions(reach=reach, status=self._status) self.set(reach, new) return new diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index 727cf21b..2d956c02 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -25,11 +25,12 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class LateralContribution(SQLSubModel): _sub_classes = [] _id_cnt = 0 - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(LateralContribution, self).__init__() self._status = status @@ -39,7 +40,6 @@ class LateralContribution(SQLSubModel): else: self.id = id - self._name = name self._type = "" self._edge = None @@ -49,10 +49,11 @@ class LateralContribution(SQLSubModel): self._header = [] self._types = [float, float] - LateralContribution._id_cnt = max(LateralContribution._id_cnt + 1, self.id) + LateralContribution._id_cnt = max( + LateralContribution._id_cnt + 1, self.id) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE lateral_contribution( id INTEGER NOT NULL PRIMARY KEY, @@ -80,7 +81,7 @@ class LateralContribution(SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return True @classmethod @@ -99,7 +100,7 @@ class LateralContribution(SQLSubModel): return res @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): new = [] tab = data["tab"] @@ -112,9 +113,9 @@ class LateralContribution(SQLSubModel): t = row[2] ctor = cls._get_ctor_from_type(t) lc = ctor( - id = row[0], - name = row[1], - status = data['status'] + id=row[0], + name=row[1], + status=data['status'] ) lc.edge = None if row[3] != -1: @@ -141,7 +142,7 @@ class LateralContribution(SQLSubModel): return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): tab = data["tab"] execute(f"DELETE FROM lateral_contribution WHERE id = {self.id}") @@ -153,10 +154,11 @@ class LateralContribution(SQLSubModel): sql = ( "INSERT INTO " + - "lateral_contribution(id, name, type, tab, edge, begin_kp, end_kp) "+ + "lateral_contribution(id, name, type, tab, " + + "edge, begin_kp, end_kp) " + "VALUES (" + - f"{self.id}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', '{tab}', {edge}, " + + f"{self.id}, '{self._db_format(self._name)}', " + + f"'{self._db_format(self._type)}', '{tab}', {edge}, " + f"{self._begin_kp}, {self._end_kp}" + ")" ) @@ -164,12 +166,12 @@ class LateralContribution(SQLSubModel): ind = 0 for d in self._data: - data0 = self._sql_format(str(d[0])) - data1 = self._sql_format(str(d[1])) + data0 = self._db_format(str(d[0])) + data1 = self._db_format(str(d[1])) sql = ( "INSERT INTO " + - "lateral_contribution_data(ind, data0, data1, lc) "+ + "lateral_contribution_data(ind, data0, data1, lc) " + f"VALUES ({ind}, '{data0}', {data1}, {self.id})" ) execute(sql) @@ -186,8 +188,13 @@ class LateralContribution(SQLSubModel): @classmethod def time_convert(cls, data): - if type(data) == str and data.count(":") == 3: - return old_pamhyr_date_to_timestamp(data) + if type(data) is str: + if data.count(":") == 3: + return old_pamhyr_date_to_timestamp(data) + if data.count(":") == 2: + return old_pamhyr_date_to_timestamp("00:" + data) + if data.count(".") == 1: + return round(float(data)) return int(data) @@ -282,7 +289,7 @@ class LateralContribution(SQLSubModel): new_1 = self._default_1 if len(header) != 0: - for i in [0,1]: + for i in [0, 1]: for j in range(len(header)): if self._header[i] == header[j]: if i == 0: @@ -295,13 +302,13 @@ class LateralContribution(SQLSubModel): return (new_0, new_1) - def add(self, index:int): + def add(self, index: int): value = (self._default_0, self._default_1) self._data.insert(index, value) self._status.modified() return value - def insert(self, index:int, value): + def insert(self, index: int, value): self._data.insert(index, value) self._status.modified() @@ -337,11 +344,10 @@ class LateralContribution(SQLSubModel): return self.data[index] def get_range(self, _range): - l = [] + lst = [] for r in _range: - l.append(r) - return l - + lst.append(r) + return lst def _set_i_c_v(self, index, column, value): v = list(self._data[index]) @@ -349,15 +355,15 @@ class LateralContribution(SQLSubModel): self._data[index] = tuple(v) self._status.modified() - def set_i_0(self, index:int, value): + def set_i_0(self, index: int, value): self._set_i_c_v(index, 0, value) - def set_i_1(self, index:int, value): + def set_i_1(self, index: int, value): self._set_i_c_v(index, 1, value) @timer def convert(self, cls): - new = cls(name = self.name, status = self._status) + new = cls(name=self.name, status=self._status) new.edge = self.edge new.begin_kp = self.begin_kp new.end_kp = self.end_kp @@ -365,8 +371,8 @@ class LateralContribution(SQLSubModel): for i, _ in enumerate(self.data): new.add(i) - for i in [0,1]: - for j in [0,1]: + for i in [0, 1]: + for j in [0, 1]: if self._header[i] == new.header[j]: for ind, v in self.data: try: diff --git a/src/Model/LateralContribution/LateralContributionList.py b/src/Model/LateralContribution/LateralContributionList.py index 8d0ba37d..a6686b97 100644 --- a/src/Model/LateralContribution/LateralContributionList.py +++ b/src/Model/LateralContribution/LateralContributionList.py @@ -27,6 +27,7 @@ from Model.LateralContribution.LateralContributionTypes import ( NotDefined, LateralContrib, Rain, Evaporation, ) + class LateralContributionList(PamhyrModelListWithTab): _tabs_list = ["liquid", "solid", "suspenssion"] _sub_classes = [ @@ -34,21 +35,21 @@ class LateralContributionList(PamhyrModelListWithTab): ] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data['status']) + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) if data is None: data = {} for tab in new._tabs: data["tab"] = tab - new._tabs[tab] = LateralContribution._sql_load( + new._tabs[tab] = LateralContribution._db_load( execute, data ) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): execute("DELETE FROM lateral_contribution") if data is None: @@ -57,7 +58,7 @@ class LateralContributionList(PamhyrModelListWithTab): for tab in self._tabs: data["tab"] = tab for lc in self._tabs[tab]: - lc._sql_save(execute, data = data) + lc._db_save(execute, data=data) return True @@ -70,16 +71,16 @@ class LateralContributionList(PamhyrModelListWithTab): def __copy__(self): new = LateralContributionList() - for l in self._tabs: - new.tabs[l] = self._tabs[l].copy() + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].copy() return new def __deepcopy__(self): new = LateralContributionList() - for l in self._tabs: - new.tabs[l] = self._tabs[l].deepcopy() + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].deepcopy() return new diff --git a/src/Model/LateralContribution/LateralContributionTypes.py b/src/Model/LateralContribution/LateralContributionTypes.py index d669266a..4f0a843c 100644 --- a/src/Model/LateralContribution/LateralContributionTypes.py +++ b/src/Model/LateralContribution/LateralContributionTypes.py @@ -22,7 +22,7 @@ from Model.LateralContribution.LateralContribution import LateralContribution class NotDefined(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(NotDefined, self).__init__(id=id, name=name, status=status) self._type = "ND" @@ -32,8 +32,9 @@ class NotDefined(LateralContribution): def _default_0(self): return 0.0 + class LateralContrib(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(LateralContrib, self).__init__(id=id, name=name, status=status) self._type = "LC" @@ -44,8 +45,9 @@ class LateralContrib(LateralContribution): def compatibility(cls): return ["liquid"] + class Rain(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(Rain, self).__init__(id=id, name=name, status=status) self._type = "RA" @@ -56,8 +58,9 @@ class Rain(LateralContribution): def compatibility(cls): return ["liquid"] + class Evaporation(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(Evaporation, self).__init__(id=id, name=name, status=status) self._type = "EV" diff --git a/src/Model/Network/Edge.py b/src/Model/Network/Edge.py index 12249943..440e03eb 100644 --- a/src/Model/Network/Edge.py +++ b/src/Model/Network/Edge.py @@ -18,13 +18,14 @@ from Model.Network.Node import Node + class Edge(object): _id_cnt = 0 - def __init__(self, id:int, name:str, - node1:Node = None, - node2:Node = None, - status = None): + def __init__(self, id: int, name: str, + node1: Node = None, + node2: Node = None, + status=None): super(Edge, self).__init__() self._status = status @@ -74,7 +75,10 @@ class Edge(object): @property def name(self): - return self._name if self._name != "" else f"{self.node1.name} -> {self.node2.name}" + name = self._name + if self._name == "": + name = f"{self.node1.name} -> {self.node2.name}" + return name def is_enable(self): return self._enable diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py index 62948a2b..c96345b1 100644 --- a/src/Model/Network/Graph.py +++ b/src/Model/Network/Graph.py @@ -21,8 +21,9 @@ from functools import reduce from Model.Network.Node import Node from Model.Network.Edge import Edge + class Graph(object): - def __init__(self, status = None): + def __init__(self, status=None): super(Graph, self).__init__() self._status = status @@ -83,7 +84,7 @@ class Graph(object): False ) - def node(self, node_name:str): + def node(self, node_name: str): node = list( filter( lambda n: n.name == node_name, @@ -96,7 +97,7 @@ class Graph(object): return node[0] - def edge(self, edge_name:str): + def edge(self, edge_name: str): edge = list( filter( lambda e: e.name == edge_name, @@ -109,12 +110,12 @@ class Graph(object): return edge[0] - def _create_node(self, x:float, y:float): + def _create_node(self, x: float, y: float): node = self._node_ctor( -1, "", - x = x, y = y, - status = self._status + x=x, y=y, + status=self._status ) return node @@ -123,14 +124,14 @@ class Graph(object): self._status.modified() return node - def add_node(self, x:float = 0.0, y:float = 0.0): + def add_node(self, x: float = 0.0, y: float = 0.0): node = self._create_node(x, y) return self._add_node(node) def insert_node(self, node): return self._add_node(node) - def remove_node(self, node_name:str): + def remove_node(self, node_name: str): self._nodes = list( filter( lambda n: n.name != node_name, @@ -139,19 +140,18 @@ class Graph(object): ) self._status.modified() - def create_node(self, x:float = 0.0, y:float = 0.0): + def create_node(self, x: float = 0.0, y: float = 0.0): node = self._create_node(x, y) return node - def _create_edge(self, n1:Node, n2:Node): + def _create_edge(self, n1: Node, n2: Node): edge = self._edge_ctor( -1, "", n1, n2, - status = self._status + status=self._status ) return edge - def _add_edge(self, edge): # This edge already exists ? if any(filter(lambda e: (e.node1 == edge.node1 and @@ -164,17 +164,17 @@ class Graph(object): self._status.modified() return edge - def add_edge(self, n1:Node, n2:Node): + def add_edge(self, n1: Node, n2: Node): edge = self._create_edge(n1, n2) return self._add_edge(edge) def insert_edge(self, edge): return self._add_edge(edge) - def create_edge(self, n1:Node, n2:Node): + def create_edge(self, n1: Node, n2: Node): return self._create_edge(n1, n2) - def remove_edge(self, edge_name:str): + def remove_edge(self, edge_name: str): self._edges = list( filter( lambda e: e.name != edge_name, @@ -212,3 +212,16 @@ class Graph(object): def is_enable_edge(self, edge): return edge._enable + + # def get_edge_id(self, reach): + # for i, e in enumerate(self.enable_edges): + # if e.id == reach.id: + # return i + + def get_edge_id(self, reach): + return next( + filter( + lambda e: e[1].id == reach.id, + enumerate(self.enable_edges()) + ) + )[0] diff --git a/src/Model/Network/Node.py b/src/Model/Network/Node.py index 7b8f52ba..d79b71cf 100644 --- a/src/Model/Network/Node.py +++ b/src/Model/Network/Node.py @@ -18,12 +18,13 @@ from Model.Network.Point import Point + class Node(object): _id_cnt = 0 - def __init__(self, id:int, name:str, - x:float = 0.0, y:float = 0.0, - status = None): + def __init__(self, id: int, name: str, + x: float = 0.0, y: float = 0.0, + status=None): super(Node, self).__init__() self._status = status @@ -73,7 +74,6 @@ class Node(object): def y(self): return self.pos.y - def setPos(self, x, y): self.pos.x = x self.pos.y = y diff --git a/src/Model/Network/Point.py b/src/Model/Network/Point.py index 0bb12609..38f0c83f 100644 --- a/src/Model/Network/Point.py +++ b/src/Model/Network/Point.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- class Point(object): - def __init__(self, x:float, y:float): + def __init__(self, x: float, y: float): super(Point, self).__init__() self.x = x diff --git a/src/Model/Reservoir/Reservoir.py b/src/Model/Reservoir/Reservoir.py new file mode 100644 index 00000000..ebf7e24c --- /dev/null +++ b/src/Model/Reservoir/Reservoir.py @@ -0,0 +1,272 @@ +# Reservoir.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from Model.Tools.PamhyrDB import SQLSubModel + +logger = logging.getLogger() + + +class Reservoir(SQLSubModel): + _sub_classes = [] + _id_cnt = 0 + + def __init__(self, id: int = -1, name: str = "", + status=None): + super(Reservoir, self).__init__() + + self._status = status + + if id == -1: + self.id = Reservoir._id_cnt + else: + self.id = id + + self._name = name + self._node = None + self._data = [] + + Reservoir._id_cnt = max(Reservoir._id_cnt + 1, self.id) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE reservoir( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + node INTEGER, + FOREIGN KEY(node) REFERENCES river_node(id) + ) + """) + + execute(""" + CREATE TABLE reservoir_data( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTEGER NOT NULL, + elevation REAL NOT NULL, + surface REAL NOT NULL, + reservoir INTEGER, + FOREIGN KEY(reservoir) REFERENCES reservoir(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 5: + cls._db_create(execute) + + return True + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, name, node " + + "FROM reservoir " + ) + + for row in table: + id = row[0] + name = row[1] + node_id = row[2] + new_reservoir = cls(id, name, status=data["status"]) + + new_reservoir._node = None + if node_id != -1: + new_reservoir._node = next( + filter( + lambda n: n.id == node_id, data["nodes"] + ) + ) + + new_data = [] + table = execute( + "SELECT elevation, surface " + + "FROM reservoir_data " + + f"WHERE reservoir = {id} " + + "ORDER BY ind ASC" + ) + for t in table: + new_data.append((t[0], t[1])) + + new_reservoir._data = new_data + + new.append(new_reservoir) + + return new + + def _db_save(self, execute, data=None): + + execute(f"DELETE FROM reservoir WHERE id = {self.id}") + execute(f"DELETE FROM reservoir_data WHERE reservoir = {self.id}") + + node_id = -1 + if self._node is not None: + node_id = self._node.id + + sql = ( + "INSERT INTO " + + "reservoir(id, name, node) " + + "VALUES (" + + f"{self.id}, '{self._db_format(self._name)}', " + + f"{node_id}" + + ")" + ) + execute(sql) + + ind = 0 + for d in self._data: + sql = ( + "INSERT INTO " + + "reservoir_data(ind, elevation, surface, reservoir) " + + f"VALUES ({ind}, '{d[0]}', {d[1]}, {self.id})" + ) + execute(sql) + ind += 1 + + return True + + def __len__(self): + return len(self._data) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + self._status.modified() + + @property + def node(self): + return self._node + + @node.setter + def node(self, node): + self._node = node + self._status.modified() + + def has_node(self): + return self._node is not None + + @property + def data(self): + return self._data.copy() + + @property + def _default_elevation(self): + return 0.0 + + @property + def _default_surface(self): + return 0.0 + + def is_define(self): + return len(self._data) != 0 + + def new_from_data(self, data): + + try: + new_0 = float(data[0]) + new_1 = float(data[1]) + except Exception as e: + logger.error(e) + new_0 = None + new_1 = None + + return (new_0, new_1) + + def add(self, index: int): + value = (self._default_elevation, self._default_surface) + self._data.insert(index, value) + self._status.modified() + return value + + def insert(self, index: int, value): + self._data.insert(index, value) + self._status.modified() + + def delete_i(self, indexes): + self._data = list( + map( + lambda e: e[1], + filter( + lambda e: e[0] not in indexes, + enumerate(self.data) + ) + ) + ) + self._status.modified() + + def delete(self, els): + self._data = list( + filter( + lambda e: e not in els, + self.data + ) + ) + self._status.modified() + + def sort(self, _reverse=False, key=None): + if key is None: + self._data.sort(reverse=_reverse) + else: + self._data.sort(reverse=_reverse, key=key) + self._status.modified() + + def get_i(self, index): + return self.data[index] + + def get_range(self, _range): + lst = [] + for r in _range: + lst.append(r) + return lst + + def _set_i_c_v(self, index, column, value): + v = list(self._data[index]) + v[column] = value + self._data[index] = tuple(v) + self._status.modified() + + def set_i_elevation(self, index: int, value): + self._set_i_c_v(index, 0, value) + + def set_i_surface(self, index: int, value): + self._set_i_c_v(index, 1, value) + + def move_up(self, index): + if index < len(self): + next = index - 1 + d = self._data + d[index], d[next] = d[next], d[index] + self._status.modified() + + def move_down(self, index): + if index >= 0: + prev = index + 1 + d = self._data + d[index], d[prev] = d[prev], d[index] + self._status.modified() diff --git a/src/Model/Reservoir/ReservoirList.py b/src/Model/Reservoir/ReservoirList.py new file mode 100644 index 00000000..60d2b98e --- /dev/null +++ b/src/Model/Reservoir/ReservoirList.py @@ -0,0 +1,87 @@ +# ReservoirList.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +from Model.Tools.PamhyrList import PamhyrModelList +from Model.Reservoir.Reservoir import Reservoir + + +class ReservoirList(PamhyrModelList): + _sub_classes = [ + Reservoir, + ] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + new._lst = Reservoir._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute("DELETE FROM reservoir") + execute("DELETE FROM reservoir_data") + + if data is None: + data = {} + + for reservoir in self._lst: + reservoir._db_save(execute, data=data) + + return True + + def new(self, index): + r = Reservoir(status=self._status) + self._lst.insert(index, r) + self._status.modified() + return r + + def __copy__(self): + new = ReservoirList() + + new._lst = self._lst.copy() + + return new + + def __deepcopy__(self): + new = ReservoirList() + + new._lst = self._lst.deepcopy() + + return new + + def copy(self): + return copy(self) + + def get_assoc_to_node(self, node): + assoc = list( + filter( + lambda i: i.node is node, + self._lst + ) + ) + + if len(assoc) > 0: + return assoc[0] + + return None diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 67698f4e..b8dff0cc 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -24,9 +24,15 @@ from Model.Results.River.River import River logger = logging.getLogger() + class Results(object): - def __init__(self, study = None): + def __init__(self, study=None, solver=None, + repertory="", name="0"): self._study = study + self._solver = solver + self._repertory = repertory + self._name = name + self._river = River(self._study) self._meta_data = { @@ -43,8 +49,19 @@ class Results(object): def river(self): return self._river + @property + def study(self): + return self._study + def set(self, key, value): self._meta_data[key] = value def get(self, key): return self._meta_data[key] + + def reload(self): + return self._solver.results( + self._study, + self._repertory, + qlog=None, + ) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 9f72f731..7597d751 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -20,11 +20,15 @@ from datetime import datetime logger = logging.getLogger() + class Profile(object): def __init__(self, profile, study): self._study = study - self._profile = profile # Source profile in the study - self._data = {} # Dict of dict {: {: , ...}, ...} + self._profile = profile # Source profile in the study + self._data = {} # Dict of dict {: {: , ...}, ...} + + def __len__(self): + return len(self._data) @property def name(self): @@ -55,10 +59,11 @@ class Profile(object): def get_ts_key(self, timestamp, key): return self._data[timestamp][key] + class Reach(object): def __init__(self, reach, study): self._study = study - self._reach = reach # Source reach in the study + self._reach = reach # Source reach in the study self._profiles = list( map( lambda p: Profile(p, self._study), @@ -66,6 +71,9 @@ class Reach(object): ) ) + def __len__(self): + return len(self._profiles) + @property def name(self): return self._reach.name @@ -84,6 +92,7 @@ class Reach(object): def set(self, profile_id, timestamp, key, data): self._profiles[profile_id].set(timestamp, key, data) + class River(object): def __init__(self, study): self._study = study @@ -91,6 +100,9 @@ class River(object): # Dict with timestamps as key self._reachs = [] + def __len__(self): + return len(self._reachs) + @property def reachs(self): return self._reachs.copy() diff --git a/src/Model/River.py b/src/Model/River.py index d5dccb9e..99cf8d21 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -28,31 +28,38 @@ from Model.Geometry.Profile import Profile from Model.Geometry.Reach import Reach from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList -from Model.LateralContribution.LateralContributionList import LateralContributionList +from Model.LateralContribution.LateralContributionList import ( + LateralContributionList +) from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict from Model.Stricklers.StricklersList import StricklersList from Model.Friction.FrictionList import FrictionList from Model.SolverParameters.SolverParametersList import SolverParametersList from Model.SedimentLayer.SedimentLayerList import SedimentLayerList +from Model.Reservoir.ReservoirList import ReservoirList +from Model.HydraulicStructures.HydraulicStructuresList import ( + HydraulicStructureList, +) from Solver.Solvers import solver_type_list + class RiverNode(Node, SQLSubModel): _sub_classes = [] - def __init__(self, id:str, name:str, - x:float, y:float, - status = None): + def __init__(self, id: str, name: str, + x: float, y: float, + status=None): super(RiverNode, self).__init__( id, name, x, y, - status = status + status=status ) self._locker = None @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE river_node( id INTEGER NOT NULL PRIMARY KEY, @@ -66,11 +73,11 @@ class RiverNode(Node, SQLSubModel): return True @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return True @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): nodes = [] table = execute("SELECT id, name, x, y FROM river_node") @@ -82,11 +89,11 @@ class RiverNode(Node, SQLSubModel): return nodes - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): sql = ( "INSERT OR REPLACE INTO river_node(id, name, x, y) VALUES (" + - f"{self.id}, '{self._sql_format(self.name)}', " + - f"{self.x}, {self.y}"+ + f"{self.id}, '{self._db_format(self.name)}', " + + f"{self.x}, {self.y}" + ")" ) execute(sql) @@ -108,21 +115,21 @@ class RiverReach(Edge, SQLSubModel): FrictionList, ] - def __init__(self, id:str, name:str, - node1:RiverNode = None, - node2:RiverNode = None, - status = None): + def __init__(self, id: str, name: str, + node1: RiverNode = None, + node2: RiverNode = None, + status=None): super(RiverReach, self).__init__( id, name, node1, node2, - status = status + status=status ) self._reach = Reach(status=self._status, parent=self) - self._frictions = FrictionList(status = self._status) + self._frictions = FrictionList(status=self._status) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE river_reach( id INTEGER NOT NULL PRIMARY KEY, @@ -139,17 +146,20 @@ class RiverReach(Edge, SQLSubModel): return True @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): reachs = [] if data is None: data = {} - table = execute("SELECT id, name, enable, node1, node2 FROM river_reach") + table = execute( + "SELECT id, name, enable, node1, node2 FROM river_reach" + ) + for row in table: # Update id counter cls._id_cnt = max(cls._id_cnt, row[0]) @@ -161,27 +171,26 @@ class RiverReach(Edge, SQLSubModel): node1 = next(filter(lambda n: n.id == row[3], data["nodes"])) node2 = next(filter(lambda n: n.id == row[4], data["nodes"])) - new = cls(id, name, node1, node2, status = data["status"]) - new.enable(enable = enable) + new = cls(id, name, node1, node2, status=data["status"]) + new.enable(enable=enable) - data["reach"] = id - data["parent"] = new - new._reach = Reach._sql_load(execute, data) + data["reach"] = new - new._frictions = FrictionList._sql_load(execute, data) + new._reach = Reach._db_load(execute, data) + new._frictions = FrictionList._db_load(execute, data) reachs.append(new) return reachs - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): sql = ( "INSERT OR REPLACE INTO " + - "river_reach(id, name, enable, node1, node2) "+ + "river_reach(id, name, enable, node1, node2) " + "VALUES (" + - f"{self.id}, '{self._sql_format(self._name)}', " + - f"{self._sql_format(self.is_enable())}," - f"{self.node1.id}, {self.node2.id}"+ + f"{self.id}, '{self._db_format(self._name)}', " + + f"{self._db_format(self.is_enable())}," + f"{self.node1.id}, {self.node2.id}" + ")" ) execute(sql) @@ -202,6 +211,7 @@ class RiverReach(Edge, SQLSubModel): def frictions(self): return self._frictions + class River(Graph, SQLSubModel): _sub_classes = [ RiverNode, @@ -212,6 +222,8 @@ class River(Graph, SQLSubModel): StricklersList, SolverParametersList, SedimentLayerList, + ReservoirList, + HydraulicStructureList, ] def __init__(self, status=None): @@ -223,87 +235,105 @@ class River(Graph, SQLSubModel): self._current_reach = None self._boundary_condition = BoundaryConditionList(status=self._status) - self._lateral_contribution = LateralContributionList(status=self._status) + self._lateral_contribution = LateralContributionList( + status=self._status) self._initial_conditions = InitialConditionsDict(status=self._status) self._stricklers = StricklersList(status=self._status) self._parameters = {} self._sediment_layers = SedimentLayerList(status=self._status) + self._reservoir = ReservoirList(status=self._status) + self._hydraulic_structures = HydraulicStructureList( + status=self._status + ) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): cls._create_submodel(execute) return True @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): cls._update_submodel(execute, version) return True @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): new = cls(data["status"]) # Stricklers (Stricklers is load in first because it's needed # for reachs) - new._stricklers = StricklersList._sql_load( + new._stricklers = StricklersList._db_load( execute, data ) data["stricklers"] = new._stricklers - # Initial conditions - new._sediment_layers = SedimentLayerList._sql_load( + new._sediment_layers = SedimentLayerList._db_load( execute, data ) data["sediment_layers_list"] = new._sediment_layers # Network - new._nodes = RiverNode._sql_load( + new._nodes = RiverNode._db_load( execute, data ) data["nodes"] = new.nodes() - new._edges = RiverReach._sql_load( + new._edges = RiverReach._db_load( execute, data ) data["edges"] = new.edges() # Boundary Condition - new._boundary_condition = BoundaryConditionList._sql_load( + new._boundary_condition = BoundaryConditionList._db_load( execute, data ) # Lateral Contribution - new._lateral_contribution = LateralContributionList._sql_load( + new._lateral_contribution = LateralContributionList._db_load( execute, data ) # Initial conditions - new._initial_conditions = InitialConditionsDict._sql_load( + new._initial_conditions = InitialConditionsDict._db_load( + execute, + data + ) + + # Reservoir + new._reservoir = ReservoirList._db_load( + execute, + data + ) + + # Hydraulic Structures + new._hydraulic_structures = HydraulicStructureList._db_load( execute, data ) # Parameters - new._parameters = SolverParametersList._sql_load( + new._parameters = SolverParametersList._db_load( execute, data ) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): objs = (self._nodes + self._edges) objs.append(self._boundary_condition) objs.append(self._initial_conditions) objs.append(self._lateral_contribution) objs.append(self._sediment_layers) objs.append(self._stricklers) + objs.append(self._reservoir) + objs.append(self._hydraulic_structures) for solver in self._parameters: objs.append(self._parameters[solver]) @@ -344,6 +374,13 @@ class River(Graph, SQLSubModel): return ret[0] + @property + def reservoir(self): + return self._reservoir + + @property + def hydraulic_structures(self): + return self._hydraulic_structures @property def parameters(self): @@ -354,8 +391,8 @@ class River(Graph, SQLSubModel): return self._parameters[solver] new = SolverParametersList( - solver_type = solver_type_list[solver], - status = self._status + solver_type=solver_type_list[solver], + status=self._status ) self._parameters[solver] = new self._status.modified() @@ -381,7 +418,7 @@ class River(Graph, SQLSubModel): has = len(self._sediment_layers) != 0 has &= any( filter( - lambda p: p.sl != None, + lambda p: p.sl is not None, flatten( map(lambda e: e.reach.profiles, self.edges()) ) diff --git a/src/Model/Saved.py b/src/Model/Saved.py index 60013cde..52bd0780 100644 --- a/src/Model/Saved.py +++ b/src/Model/Saved.py @@ -20,6 +20,7 @@ import logging logger = logging.getLogger() + class SavedStatus(object): def __init__(self): super(SavedStatus, self).__init__() diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 1438dfa7..0a994b36 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -5,16 +5,17 @@ from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel from Model.Except import NotImplementedMethodeError + class Layer(SQLSubModel): _sub_classes = [] _id_cnt = 0 def __init__(self, - id:int = -1, name:str = "", - type = "", - height = 0.0, d50 = 0.0, sigma = 0.0, - critical_constraint = 0.0, - sl = None, status = None): + id: int = -1, name: str = "", + type="", + height=0.0, d50=0.0, sigma=0.0, + critical_constraint=0.0, + sl=None, status=None): super(Layer, self).__init__() self._status = status @@ -83,7 +84,7 @@ class Layer(SQLSubModel): self._critical_constraint = float(critical_constraint) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE sedimentary_layer_layer( id INTEGER NOT NULL PRIMARY KEY, @@ -102,21 +103,22 @@ class Layer(SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): major, minor, release = version.strip().split(".") if major == minor == "0": if int(release) < 2: - cls._sql_create(execute) + cls._db_create(execute) return True @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): new = [] sl = data["sl"] table = execute( - "SELECT id, ind, name, type, height, d50, sigma, critical_constraint " + + "SELECT id, ind, name, type, height, " + + "d50, sigma, critical_constraint " + "FROM sedimentary_layer_layer " + f"WHERE sl = {sl}" ) @@ -128,27 +130,28 @@ class Layer(SQLSubModel): ind = row[1] layer = cls( - id = row[0], name = row[2], - type = row[3], height = row[4], - d50 = row[5], sigma = row[6], - critical_constraint = row[7], - sl = sl, status = data['status'] + id=row[0], name=row[2], + type=row[3], height=row[4], + d50=row[5], sigma=row[6], + critical_constraint=row[7], + sl=sl, status=data['status'] ) new[ind] = layer return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): ind = data["ind"] sl = data["sl"] sql = ( "INSERT INTO " + - "sedimentary_layer_layer(id, ind, name, type, height, d50, sigma, critical_constraint, sl) "+ + "sedimentary_layer_layer(id, ind, name, type, height, " + + "d50, sigma, critical_constraint, sl) " + "VALUES (" + - f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', {self._height}, " + + f"{self.id}, {ind}, '{self._db_format(self._name)}', " + + f"'{self._db_format(self._type)}', {self._height}, " + f"{self._d50}, {self._sigma}, {self._critical_constraint}, " + f"{sl.id}" + ")" @@ -162,9 +165,9 @@ class SedimentLayer(SQLSubModel): _sub_classes = [Layer] _id_cnt = 0 - def __init__(self, id:int = -1, - name:str = "", comment:str = "", - status = None): + def __init__(self, id: int = -1, + name: str = "", comment: str = "", + status=None): super(SedimentLayer, self).__init__() self._status = status @@ -196,7 +199,7 @@ class SedimentLayer(SQLSubModel): def height(self): return list( - map(lambda l: l.height, self._layers) + map(lambda layer: layer.height, self._layers) ) @property @@ -209,7 +212,7 @@ class SedimentLayer(SQLSubModel): def names(self): return list( - map(lambda l: l.name, self._layers) + map(lambda layer: layer.name, self._layers) ) @property @@ -221,7 +224,7 @@ class SedimentLayer(SQLSubModel): self._comment = comment @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE sedimentary_layer( id INTEGER NOT NULL PRIMARY KEY, @@ -233,16 +236,16 @@ class SedimentLayer(SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): major, minor, release = version.strip().split(".") if major == minor == "0": if int(release) < 2: - cls._sql_create(execute) + cls._db_create(execute) return True @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): new = [] table = execute( @@ -252,36 +255,36 @@ class SedimentLayer(SQLSubModel): for row in table: sl = cls( - id = row[0], - name = row[1], - comment = row[2], - status = data['status'] + id=row[0], + name=row[1], + comment=row[2], + status=data['status'] ) data["sl"] = sl.id - sl._layers = Layer._sql_load(execute, data) + sl._layers = Layer._db_load(execute, data) new.append(sl) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): if data is None: data = {} sql = ( "INSERT INTO sedimentary_layer (id, name, comment) " + - f"VALUES ({self.id}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._comment)}')" + f"VALUES ({self.id}, '{self._db_format(self._name)}', " + + f"'{self._db_format(self._comment)}')" ) execute(sql) data["sl"] = self ind = 0 - for l in self._layers: + for layer in self._layers: data["ind"] = ind - l._sql_save(execute, data) + layer._db_save(execute, data) ind += 1 return True @@ -298,7 +301,7 @@ class SedimentLayer(SQLSubModel): self._status.modified() def new(self, index): - n = Layer(sl=self, status = self._status) + n = Layer(sl=self, status=self._status) self.insert(index, n) self._status.modified() return n @@ -324,8 +327,8 @@ class SedimentLayer(SQLSubModel): if index >= 0: next = index - 1 - l = self._layers - l[index], l[next] = l[next], l[index] + lst = self._layers + lst[index], lst[next] = lst[next], lst[index] self._status.modified() @@ -333,7 +336,7 @@ class SedimentLayer(SQLSubModel): if index + 1 < len(self._layers): prev = index + 1 - l = self._layers - l[index], l[prev] = l[prev], l[index] + lst = self._layers + lst[index], lst[prev] = lst[prev], lst[index] self._status.modified() diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py index 53a1a8fd..78ba611c 100644 --- a/src/Model/SedimentLayer/SedimentLayerList.py +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -6,18 +6,19 @@ from Model.Except import NotImplementedMethodeError from Model.Tools.PamhyrList import PamhyrModelList from Model.SedimentLayer.SedimentLayer import SedimentLayer + class SedimentLayerList(PamhyrModelList): _sub_classes = [SedimentLayer] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"]) + def _db_load(cls, execute, data=None): + new = cls(status=data["status"]) - new._lst = SedimentLayer._sql_load(execute, data) + new._lst = SedimentLayer._db_load(execute, data) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): ok = True # Delete previous data @@ -25,7 +26,7 @@ class SedimentLayerList(PamhyrModelList): execute("DELETE FROM sedimentary_layer_layer") for sl in self._lst: - ok &= sl._sql_save(execute, data) + ok &= sl._db_save(execute, data) return ok @@ -34,7 +35,7 @@ class SedimentLayerList(PamhyrModelList): return self.lst def new(self, index): - n = SedimentLayer(status = self._status) + n = SedimentLayer(status=self._status) self.insert(index, n) self._status.modified() return n diff --git a/src/Model/Serializable.py b/src/Model/Serializable.py index 0c083448..fb4ea11d 100644 --- a/src/Model/Serializable.py +++ b/src/Model/Serializable.py @@ -18,6 +18,7 @@ import pickle + class Serializable(): def __init__(self): return diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index ce7ade98..bf1ca817 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -22,10 +22,11 @@ from Solver.Solvers import solver_type_list from Model.Tools.PamhyrList import PamhyrModelList + class Parameter(): def __init__(self, - name = "", value = "", - status = None): + name="", value="", + status=None): self._status = status self._name = name @@ -57,14 +58,15 @@ class Parameter(): @classmethod def from_tuple(cls, data, status): - new = cls(status = status) + new = cls(status=status) new["name"] = data[0] new["value"] = data[1] return new + class SolverParametersList(PamhyrModelList): - def __init__(self, solver_type = None, status = None): + def __init__(self, solver_type=None, status=None): super(SolverParametersList, self).__init__() self._status = status @@ -76,9 +78,8 @@ class SolverParametersList(PamhyrModelList): ) ) - @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE solver_parameter( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -92,40 +93,47 @@ class SolverParametersList(PamhyrModelList): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): major, minor, release = version.strip().split(".") if major == minor == "0": if int(release) < 3: - execute(f"UPDATE solver_parameter SET name='mage_implicitation' WHERE name='mage_implication'") - execute(f"UPDATE solver_parameter SET name='mage_iteration_type' WHERE name='mage_iter_type'") + execute( + "UPDATE solver_parameter SET name='mage_implicitation' " + + "WHERE name='mage_implication'" + ) + execute( + "UPDATE solver_parameter SET name='mage_iteration_type' " + + "WHERE name='mage_iter_type'" + ) if int(release) < 4: - solvers = execute("SELECT DISTINCT solver FROM solver_parameter") + solvers = execute( + "SELECT DISTINCT solver FROM solver_parameter") for solver in solvers: if solver != "mage8": continue data = execute("SELECT ind, name, value " + - "FROM solver_parameter "+ - f"WHERE solver = '{solver}' " + - "ORDER BY ind ACS") + "FROM solver_parameter " + + f"WHERE solver = '{solver}' " + + "ORDER BY ind ACS") execute( "DELETE FROM solver_parameter " + f"WHERE solver = '{solver}'" ) - insert = lambda name, value, ind: execute( - "INSERT INTO " + - "solver_parameter(ind, name, value, solver) "+ - "VALUES (" + - f"{ind}, " + - f"'{cls._sql_format(name)}', " + - f"'{cls._sql_format(value)}', " + - f"'{cls._sql_format(solver)}'" + - ")" + def insert(name, value, ind): return execute( + "INSERT INTO " + + "solver_parameter(ind, name, value, solver) " + + "VALUES (" + + f"{ind}, " + + f"'{cls._db_format(name)}', " + + f"'{cls._db_format(value)}', " + + f"'{cls._db_format(solver)}'" + + ")" ) ind = 0 @@ -134,6 +142,10 @@ class SolverParametersList(PamhyrModelList): insert(v[0], v[1], ind) ind += 1 + if int(release) < 7: + insert("mage_init_internal", "N", ind) + ind += 1 + new = [ ("mage_sediment_masse_volumique", "2650.0"), ("mage_sediment_angle_repos", "40.0"), @@ -156,7 +168,7 @@ class SolverParametersList(PamhyrModelList): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): status = data["status"] solvers = execute("SELECT DISTINCT solver FROM solver_parameter") new = {} @@ -167,7 +179,7 @@ class SolverParametersList(PamhyrModelList): continue st = solver_type_list[solver] - n = cls(solver_type = st, status = status) + n = cls(solver_type=st, status=status) table = execute( "SELECT ind, name, value " + @@ -186,7 +198,7 @@ class SolverParametersList(PamhyrModelList): return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): t = self._solver._type execute( "DELETE FROM solver_parameter " + @@ -197,12 +209,12 @@ class SolverParametersList(PamhyrModelList): for param in self._lst: sql = ( "INSERT INTO " + - "solver_parameter(ind, name, value, solver) "+ + "solver_parameter(ind, name, value, solver) " + "VALUES (" + f"{ind}, " + - f"'{self._sql_format(param.name)}', " + - f"'{self._sql_format(param.value)}', " + - f"'{self._sql_format(t)}'" + + f"'{self._db_format(param.name)}', " + + f"'{self._db_format(param.value)}', " + + f"'{self._db_format(t)}'" + ")" ) execute(sql) @@ -225,7 +237,7 @@ class SolverParametersList(PamhyrModelList): self._lst ) )["value"] - except: + except Exception: return None def set(self, index, new): @@ -241,15 +253,15 @@ class SolverParametersList(PamhyrModelList): self._lst.append( Parameter( - name = key, - value = value, - status = self._status + name=key, + value=value, + status=self._status ) ) self._status.modified() def new(self, index): - n = Parameter(status = self._status) + n = Parameter(status=self._status) self._lst.insert(index, n) self._status.modified() return n diff --git a/src/Model/Stricklers/Stricklers.py b/src/Model/Stricklers/Stricklers.py index e18fd01d..10b8aaf3 100644 --- a/src/Model/Stricklers/Stricklers.py +++ b/src/Model/Stricklers/Stricklers.py @@ -20,16 +20,17 @@ from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel + class Stricklers(SQLSubModel): _id_cnt = 0 _sub_classes = [] - def __init__(self, id:int = -1, - name:str = "", - comment:str = "", - minor:float = 35.0, - medium:float = 15.0, - status = None): + def __init__(self, id: int = -1, + name: str = "", + comment: str = "", + minor: float = 35.0, + medium: float = 15.0, + status=None): super(Stricklers, self).__init__() self._status = status @@ -46,9 +47,8 @@ class Stricklers(SQLSubModel): self._minor = minor self._medium = medium - @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): execute(""" CREATE TABLE stricklers( id INTEGER NOT NULL PRIMARY KEY, @@ -62,11 +62,11 @@ class Stricklers(SQLSubModel): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): stricklers = [] status = data["status"] @@ -86,25 +86,25 @@ class Stricklers(SQLSubModel): medium = row[4] new = cls( - id = id, - name = name, - comment = comment, - minor = minor, medium = medium, - status = status + id=id, + name=name, + comment=comment, + minor=minor, medium=medium, + status=status ) stricklers.append(new) return stricklers - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): sql = ( "INSERT INTO " + - "stricklers(id, name, comment, minor, medium) "+ + "stricklers(id, name, comment, minor, medium) " + "VALUES (" + f"{self.id}, " + - f"'{self._sql_format(self.name)}', " + - f"'{self._sql_format(self.comment)}', " + + f"'{self._db_format(self.name)}', " + + f"'{self._db_format(self.comment)}', " + f"{float(self.minor)}, {float(self.medium)}" + ")" ) diff --git a/src/Model/Stricklers/StricklersList.py b/src/Model/Stricklers/StricklersList.py index b000c07e..6730dc02 100644 --- a/src/Model/Stricklers/StricklersList.py +++ b/src/Model/Stricklers/StricklersList.py @@ -22,31 +22,32 @@ from Model.Saved import SavedStatus from Model.Tools.PamhyrList import PamhyrModelList from Model.Stricklers.Stricklers import Stricklers + class StricklersList(PamhyrModelList): _sub_classes = [ Stricklers, ] @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"]) + def _db_load(cls, execute, data=None): + new = cls(status=data["status"]) - new._lst = Stricklers._sql_load( + new._lst = Stricklers._db_load( execute, - data = data + data=data ) return new - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): execute("DELETE FROM stricklers") objs = self._lst @@ -57,19 +58,21 @@ class StricklersList(PamhyrModelList): return self.lst def new(self, index): - s = Stricklers(status = self._status) + s = Stricklers(status=self._status) self.insert(index, s) return s @timer - def sort(self, reverse:bool = False, key=None): - f = lambda st: st.name + def sort(self, reverse: bool = False, key=None): + def f(st): return st.name if key is not None: f = key self._lst = sorted( self._lst, - key = f, - reverse = reverse, + key=f, + reverse=reverse, ) - self._status.modified() + + if self._status is not None: + self._status.modified() diff --git a/src/Model/Study.py b/src/Model/Study.py index f022a195..033f8867 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -33,20 +33,21 @@ from Checker.Study import * logger = logging.getLogger() + class Study(SQLModel): _sub_classes = [ River, ] - def __init__(self, filename = None, init_new = True): + def __init__(self, filename=None, init_new=True): # Metadata - self._version = "0.0.4" + self._version = "0.0.7" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() self._filename = filename - super(Study, self).__init__(filename = filename) + super(Study, self).__init__(filename=filename) self.status = SavedStatus() @@ -59,9 +60,9 @@ class Study(SQLModel): if init_new: # Study data - self._river = River(status = self.status) + self._river = River(status=self.status) else: - self._init_db_file(filename, is_new = False) + self._init_db_file(filename, is_new=False) self._old_save_id = 0 @@ -85,7 +86,7 @@ class Study(SQLModel): def is_saved(self): return self.status.is_saved() - def save(self): + def save(self, progress=None): # Save a copy of database fdir, fname = os.path.split(self.filename) @@ -109,7 +110,7 @@ class Study(SQLModel): # Save self.last_save_date = datetime.now() - self._save() + self._save(progress=progress) self.status.save() @property @@ -128,7 +129,7 @@ class Study(SQLModel): @filename.setter def filename(self, filename): self._filename = str(filename) - self._init_db_file(filename, is_new = True) + self._init_db_file(filename, is_new=True) self.status.modified() @property @@ -139,7 +140,7 @@ class Study(SQLModel): self._time_system = "time" self.status.modified() - def use_date(self, date:datetime): + def use_date(self, date: datetime): self._time_system = "date" self._date = date self.status.modified() @@ -158,7 +159,7 @@ class Study(SQLModel): # return cls() @classmethod - def new(cls, name, description, date = None): + def new(cls, name, description, date=None): me = cls() me.name = name me.description = description @@ -176,16 +177,26 @@ class Study(SQLModel): # SQL # ####### + def _db_insert_into_info(self, key, value, commit=False): + self.execute( + "INSERT INTO info VALUES " + + f"('{key}', '{self._db_format(value)}')", + commit=commit + ) + def _create(self): # Info (metadata) - self.execute("CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") self.execute( - f"INSERT INTO info VALUES ('version', '{self._sql_format(self._version)}')", - commit = True + "CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") + self.execute( + "INSERT INTO info VALUES ('version', " + + f"'{self._db_format(self._version)}')", + commit=True ) self.execute("INSERT INTO info VALUES ('name', '')") self.execute("INSERT INTO info VALUES ('description', '')") - self.execute(f"INSERT INTO info VALUES ('time_system', '{self._time_system}')") + self.execute( + f"INSERT INTO info VALUES ('time_system', '{self._time_system}')") self.execute( f"INSERT INTO info VALUES ('date', " + "'{self._date.timestamp()}')" @@ -213,7 +224,8 @@ class Study(SQLModel): logger.info("Update database") if self._update_submodel(version[0]): - self.execute(f"UPDATE info SET value='{self._version}' WHERE key='version'") + self.execute( + f"UPDATE info SET value='{self._version}' WHERE key='version'") return True logger.info("TODO: update failed") @@ -221,42 +233,91 @@ class Study(SQLModel): @classmethod def _load(cls, filename): - new = cls(init_new = False, filename = filename) + new = cls(init_new=False, filename=filename) # TODO: Load metadata new.name = new.execute("SELECT value FROM info WHERE key='name'")[0] - new.description = new.execute("SELECT value FROM info WHERE key='description'")[0] - new._time_system = new.execute("SELECT value FROM info WHERE key='time_system'")[0] + new.description = new.execute( + "SELECT value FROM info WHERE key='description'")[0] + new._time_system = new.execute( + "SELECT value FROM info WHERE key='time_system'")[0] new._date = datetime.fromtimestamp( float(new.execute("SELECT value FROM info WHERE key='date'")[0]) ) new.creation_date = datetime.fromtimestamp( - float(new.execute("SELECT value FROM info WHERE key='creation_date'")[0]) + float(new.execute( + "SELECT value FROM info WHERE key='creation_date'")[0]) ) new.last_save_date = datetime.fromtimestamp( - float(new.execute("SELECT value FROM info WHERE key='last_save_date'")[0]) + float(new.execute( + "SELECT value FROM info WHERE key='last_save_date'")[0]) ) # Load river data - new._river = River._sql_load( + new._river = River._db_load( lambda sql: new.execute( sql, - fetch_one = False, - commit = True + fetch_one=False, + commit=True ), - data = {"status": new.status} + data={"status": new.status} ) return new - def _save(self): - self.execute(f"UPDATE info SET value='{self._sql_format(self.name)}' WHERE key='name'") - self.execute(f"UPDATE info SET value='{self._sql_format(self.description)}' WHERE key='description'") - self.execute(f"UPDATE info SET value='{self._time_system}' WHERE key='time_system'") - self.execute(f"UPDATE info SET value='{timestamp(self._date)}' WHERE key='date'") - self.execute(f"UPDATE info SET value='{timestamp(self.creation_date)}' WHERE key='creation_date'") - self.execute(f"UPDATE info SET value='{timestamp(self.last_save_date)}' WHERE key='last_save_date'") + def _save(self, progress=None): + progress = progress if progress is not None else lambda: None - self._save_submodel([self._river]) + self.execute( + f"UPDATE info SET " + + f"value='{self._db_format(self.name)}' WHERE key='name'" + ) + progress() + self.execute( + f"UPDATE info SET " + + f"value='{self._db_format(self.description)}' " + + "WHERE key='description'" + ) + progress() + self.execute( + f"UPDATE info SET " + + f"value='{self._time_system}' WHERE key='time_system'" + ) + progress() + self.execute( + f"UPDATE info SET " + + f"value='{timestamp(self._date)}' WHERE key='date'" + ) + progress() + self.execute( + f"UPDATE info SET " + + f"value='{timestamp(self.creation_date)}' " + + "WHERE key='creation_date'" + ) + progress() + self.execute( + f"UPDATE info SET " + + f"value='{timestamp(self.last_save_date)}' " + + "WHERE key='last_save_date'" + ) + progress() + + self._save_submodel([self._river], data=progress) self.commit() + + def sql_save_request_count(self): + return self._count() + + def _count(self): + cnt = self._save_count([self._river]) + logger.debug(cnt) + return cnt + 6 + + def close(self): + """Close db connection + + Returns: + Nothing. + """ + self._close() diff --git a/src/Model/Tools/PamhyrDB.py b/src/Model/Tools/PamhyrDB.py index 73174f94..44065709 100644 --- a/src/Model/Tools/PamhyrDB.py +++ b/src/Model/Tools/PamhyrDB.py @@ -21,6 +21,7 @@ import sqlite3 import logging from pathlib import Path +from functools import reduce from tools import SQL from Model.Except import NotImplementedMethodeError @@ -28,10 +29,12 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() # Top level model class + + class SQLModel(SQL): _sub_classes = [] - def _init_db_file(self, db, is_new = True): + def _init_db_file(self, db, is_new=True): exists = Path(db).exists() if exists and is_new: @@ -48,19 +51,18 @@ class SQLModel(SQL): self._update() # Update db scheme if necessary # self._load() # Load data - - def __init__(self, filename = None): + def __init__(self, filename=None): self._db = None def _create_submodel(self): - fn = lambda sql: self.execute( + def fn(sql): return self.execute( sql, - fetch_one = False, - commit = False + fetch_one=False, + commit=False ) for cls in self._sub_classes: - requests = cls._sql_create(fn) + requests = cls._db_create(fn) self.commit() return True @@ -69,15 +71,15 @@ class SQLModel(SQL): raise NotImplementedMethodeError(self, self._create) def _update_submodel(self, version): - fn = lambda sql: self.execute( + def fn(sql): return self.execute( sql, - fetch_one = False, - commit = False + fetch_one=False, + commit=False ) ok = True for cls in self._sub_classes: - ok &= cls._sql_update(fn, version) + ok &= cls._db_update(fn, version) self.commit() return ok @@ -85,46 +87,87 @@ class SQLModel(SQL): def _update(self): raise NotImplementedMethodeError(self, self._update) - def _save_submodel(self, objs, data = None): - fn = lambda sql: self.execute( - sql, - fetch_one = False, - commit = False - ) + def _save_submodel(self, objs, data=None): + progress = data if data is not None else lambda: None + + def fn(sql): + res = self.execute( + sql, + fetch_one=False, + commit=False + ) + progress() + return res ok = True for obj in objs: - ok &= obj._sql_save(fn) + ok &= obj._db_save(fn) self.commit() return ok - def _save(self): + def _save(self, progress=None): raise NotImplementedMethodeError(self, self._save) + def _count(self): + raise NotImplementedMethodeError(self, self._count) + + def _save_count(self, objs, data=None): + counter = { + "insert": 0, + "update": 0, + "delete": 0, + "other": 0, + } + + def fn(sql): + if "insert" in sql.lower(): + counter["insert"] = counter["insert"] + 1 + elif "update" in sql.lower(): + counter["update"] = counter["update"] + 1 + elif "delete" in sql.lower(): + counter["delete"] = counter["delete"] + 1 + else: + counter["other"] = counter["other"] + 1 + return [] + + ok = True + for obj in objs: + ok &= obj._db_save(fn) + + logger.debug(counter) + + return reduce( + lambda acc, k: acc + counter[k], + counter, + 0 + ) + @classmethod - def _load(cls, filename = None): + def _load(cls, filename=None): raise NotImplementedMethodeError(cls, cls._load) # Sub model class + + class SQLSubModel(object): _sub_classes = [] - def _sql_format(self, value): + def _db_format(self, value): # Replace ''' by ''' to preserve SQL injection - if type(value) == str: + if type(value) is str: value = value.replace("'", "'") - elif type(value) == bool: + elif type(value) is bool: value = 'TRUE' if value else 'FALSE' return value @classmethod def _create_submodel(cls, execute): for sc in cls._sub_classes: - sc._sql_create(execute) + sc._db_create(execute) @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): """Create data base scheme Args: @@ -133,15 +176,15 @@ class SQLSubModel(object): Returns: Return true, otherelse false if an issue appear """ - raise NotImplementedMethodeError(cls, cls._sql_create) + raise NotImplementedMethodeError(cls, cls._db_create) @classmethod def _update_submodel(cls, execute, version): for sc in cls._sub_classes: - sc._sql_update(execute, version) + sc._db_update(execute, version) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): """Update data base scheme Args: @@ -151,10 +194,10 @@ class SQLSubModel(object): Returns: Return true, otherelse false if an issue appear """ - raise NotImplementedMethodeError(cls, cls._sql_update) + raise NotImplementedMethodeError(cls, cls._db_update) @classmethod - def _sql_load(cls, execute, data = None): + def _db_load(cls, execute, data=None): """Load instance of this class from SQL data base Args: @@ -164,13 +207,13 @@ class SQLSubModel(object): Returns: Return new instance of class """ - raise NotImplementedMethodeError(cls, cls._sql_load) + raise NotImplementedMethodeError(cls, cls._db_load) - def _save_submodel(self, execute, objs, data = None): + def _save_submodel(self, execute, objs, data=None): for o in objs: - o._sql_save(execute, data = data) + o._db_save(execute, data=data) - def _sql_save(self, execute, data = None): + def _db_save(self, execute, data=None): """Save class data to data base Args: @@ -181,4 +224,4 @@ class SQLSubModel(object): Return true, otherelse false if an issue appear during save """ - raise NotImplementedMethodeError(self, self._sql_save) + raise NotImplementedMethodeError(self, self._db_save) diff --git a/src/Model/Tools/PamhyrDict.py b/src/Model/Tools/PamhyrDict.py index f1652516..31bc12a6 100644 --- a/src/Model/Tools/PamhyrDict.py +++ b/src/Model/Tools/PamhyrDict.py @@ -16,6 +16,7 @@ # -*- coding: utf-8 -*- +import types import logging from copy import copy @@ -30,7 +31,7 @@ logger = logging.getLogger() class PamhyrModelDict(SQLSubModel): _sub_classes = [] - def __init__(self, status = None): + def __init__(self, status=None): super(PamhyrModelDict, self).__init__() self._status = status @@ -38,19 +39,19 @@ class PamhyrModelDict(SQLSubModel): self._dict = {} @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): - raise NotImplementedMethodeError(cls, cls._sql_load) + def _db_load(cls, execute, data=None): + raise NotImplementedMethodeError(cls, cls._db_load) - def _sql_save(self, execute, data = None): - raise NotImplementedMethodeError(self, self._sql_save) + def _db_save(self, execute, data=None): + raise NotImplementedMethodeError(self, self._db_save) def __len__(self): return len(self._dict) @@ -64,7 +65,12 @@ class PamhyrModelDict(SQLSubModel): def get(self, key): if key in self._dict: - return self._dict[key] + v = self._dict[key] + + if type(v) is types.GeneratorType: + return list(v) + + return v new = self.new(key) self.set(key, new) diff --git a/src/Model/Tools/PamhyrList.py b/src/Model/Tools/PamhyrList.py index 6938343f..e043cff7 100644 --- a/src/Model/Tools/PamhyrList.py +++ b/src/Model/Tools/PamhyrList.py @@ -26,11 +26,12 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class PamhyrModelList(SQLSubModel): _sub_classes = [ ] - def __init__(self, status = None): + def __init__(self, status=None): super(PamhyrModelList, self).__init__() self._status = status @@ -41,37 +42,38 @@ class PamhyrModelList(SQLSubModel): ####### @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): - raise NotImplementedMethodeError(cls, cls._sql_load) + def _db_load(cls, execute, data=None): + raise NotImplementedMethodeError(cls, cls._db_load) - def _sql_save(self, execute, data = None): - raise NotImplementedMethodeError(self, self._sql_save) + def _db_save(self, execute, data=None): + raise NotImplementedMethodeError(self, self._db_save) ################ # MODEL METHOD # ################ - def __len__(self): - return len(self._lst) - @property def lst(self): - return self._lst.copy() + return self._lst + + def __len__(self): + return len(self.lst) def get(self, row): return self._lst[row] def set(self, row, new): self._lst[row] = new - self._status.modified() + if self._status is not None: + self._status.modified() def new(self, index): """Create new elements and add it to list @@ -86,12 +88,16 @@ class PamhyrModelList(SQLSubModel): def insert(self, index, new): self._lst.insert(index, new) - self._status.modified() + + if self._status is not None: + self._status.modified() def delete(self, lst): for el in lst: self._lst.remove(el) - self._status.modified() + + if self._status is not None: + self._status.modified() def delete_i(self, indexes): lst = list( @@ -107,23 +113,29 @@ class PamhyrModelList(SQLSubModel): def sort(self, reverse=False, key=None): self._lst.sort(reverse=reverse, key=key) - self._status.modified() + + if self._status is not None: + self._status.modified() def move_up(self, index): if index < len(self._lst): next = index - 1 - l = self._lst - l[index], l[next] = l[next], l[index] - self._status.modified() + lst = self._lst + lst[index], lst[next] = lst[next], lst[index] + + if self._status is not None: + self._status.modified() def move_down(self, index): if index >= 0: prev = index + 1 - l = self._lst - l[index], l[prev] = l[prev], l[index] - self._status.modified() + lst = self._lst + lst[index], lst[prev] = lst[prev], lst[index] + + if self._status is not None: + self._status.modified() class PamhyrModelListWithTab(SQLSubModel): @@ -131,7 +143,7 @@ class PamhyrModelListWithTab(SQLSubModel): _sub_classes = [ ] - def __init__(self, status = None): + def __init__(self, status=None): super(PamhyrModelListWithTab, self).__init__() self._status = status @@ -144,19 +156,19 @@ class PamhyrModelListWithTab(SQLSubModel): ####### @classmethod - def _sql_create(cls, execute): + def _db_create(cls, execute): return cls._create_submodel(execute) @classmethod - def _sql_update(cls, execute, version): + def _db_update(cls, execute, version): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): - raise NotImplementedMethodeError(cls, cls._sql_load) + def _db_load(cls, execute, data=None): + raise NotImplementedMethodeError(cls, cls._db_load) - def _sql_save(self, execute, data = None): - raise NotImplementedMethodeError(self, self._sql_save) + def _db_save(self, execute, data=None): + raise NotImplementedMethodeError(self, self._db_save) ################ # MODEL METHOD # @@ -216,14 +228,14 @@ class PamhyrModelListWithTab(SQLSubModel): if index < len(self._tabs[lst]): next = index - 1 - l = self._tabs[lst] - l[index], l[next] = l[next], l[index] + lst = self._tabs[lst] + lst[index], lst[next] = lst[next], lst[index] self._status.modified() def move_down(self, lst, index): if index >= 0: prev = index + 1 - l = self._tabs[lst] - l[index], l[prev] = l[prev], l[index] + lst = self._tabs[lst] + lst[index], lst[prev] = lst[prev], lst[index] self._status.modified() diff --git a/src/Model/__init__.py b/src/Model/__init__.py new file mode 100644 index 00000000..89e3fbe8 --- /dev/null +++ b/src/Model/__init__.py @@ -0,0 +1,17 @@ +# __init__.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- diff --git a/src/Model/test_Model.py b/src/Model/test_Model.py new file mode 100644 index 00000000..2694366f --- /dev/null +++ b/src/Model/test_Model.py @@ -0,0 +1,110 @@ +# test_Model.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import unittest +import tempfile + +from Model.Saved import SavedStatus +from Model.Study import Study +from Model.River import River + + +class StudyTestCase(unittest.TestCase): + def test_create_study(self): + study = Study.new("foo", "bar") + self.assertEqual(study.name, "foo") + self.assertEqual(study.description, "bar") + + def test_open_study(self): + study = Study.open("../tests_cases/Enlargement/Enlargement.pamhyr") + self.assertNotEqual(study, None) + self.assertEqual(study.name, "Enlargement") + + def test_save_open_study(self): + study = Study.new("foo", "bar") + dir = tempfile.mkdtemp() + f = os.path.join(dir, "foo.pamhyr") + + # Save study + study.filename = f + study.save() + study.close() + + # Reopen study + study = Study.open(f) + + # Check + self.assertNotEqual(study, None) + self.assertEqual(study.name, "foo") + self.assertEqual(study.description, "bar") + + def test_create_study_river(self): + study = Study.new("foo", "bar") + self.assertNotEqual(study.river, None) + + +class RiverTestCase(unittest.TestCase): + def test_create_river(self): + status = SavedStatus() + river = River(status=status) + + self.assertNotEqual(river, None) + + def test_create_river_nodes(self): + status = SavedStatus() + river = River(status=status) + + self.assertNotEqual(river, None) + + # Add nodes + n0 = river.add_node() + n1 = river.add_node(x=1.0, y=0.0) + n2 = river.add_node(x=0.0, y=1.0) + + # Checks + self.assertEqual(river.nodes_counts(), 3) + + nodes = river.nodes() + self.assertEqual(nodes[0], n0) + self.assertEqual(nodes[1], n1) + self.assertEqual(nodes[2], n2) + + def test_create_river_edges(self): + status = SavedStatus() + river = River(status=status) + + self.assertNotEqual(river, None) + + # Add nodes + n0 = river.add_node() + n1 = river.add_node(x=1.0, y=0.0) + n2 = river.add_node(x=0.0, y=1.0) + + self.assertEqual(river.nodes_counts(), 3) + + # Add edges + e0 = river.add_edge(n0, n1) + e1 = river.add_edge(n1, n2) + + # Checks + self.assertEqual(river.edges_counts(), 2) + + edges = river.edges() + self.assertEqual(edges[0], e0) + self.assertEqual(edges[1], e1) diff --git a/src/Scripts/AScript.py b/src/Scripts/AScript.py index 032f9560..66037418 100644 --- a/src/Scripts/AScript.py +++ b/src/Scripts/AScript.py @@ -18,6 +18,7 @@ from Model.Except import NotImplementedMethodeError + class AScript(object): name = "" description = "" diff --git a/src/Scripts/Hello.py b/src/Scripts/Hello.py index b1c27fe5..c7333b84 100644 --- a/src/Scripts/Hello.py +++ b/src/Scripts/Hello.py @@ -1,4 +1,4 @@ -# help.py -- Pamhyr +# Hello.py -- Pamhyr # Copyright (C) 2023 INRAE # # This program is free software: you can redistribute it and/or modify diff --git a/src/Scripts/ListSolver.py b/src/Scripts/ListSolver.py new file mode 100644 index 00000000..fe50d8d9 --- /dev/null +++ b/src/Scripts/ListSolver.py @@ -0,0 +1,38 @@ +# ListSolver.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import logging + +from Scripts.AScript import AScript + +logger = logging.getLogger() + + +class ScriptListSolver(AScript): + name = "ListSolver" + description = "List configured solver(s) for Pamhyr2" + + def usage(self): + logger.info(f"Usage : {self._args[0]} {self._args[1]}") + + def run(self): + for solver in self._conf.solvers: + print(f"{solver.name:<16} ({solver.type}): {solver.description}") + + return 0 diff --git a/src/Scripts/MageMesh.py b/src/Scripts/MageMesh.py new file mode 100644 index 00000000..065c1234 --- /dev/null +++ b/src/Scripts/MageMesh.py @@ -0,0 +1,289 @@ +# MageMesh.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import logging + +from ctypes import ( + cdll, + byref, Structure, + c_char_p, c_wchar_p, + create_string_buffer, + POINTER, c_void_p, + c_int, c_double, c_bool +) + +from Scripts.AScript import AScript + +logger = logging.getLogger() + +bief_lib = None +c_init_bief_from_geo_file = None +c_get_nb_sections = None +c_get_nb_points_section = None +c_set_bief_name = None +c_st_to_m_compl = None +c_interpolate_profils_pas_transversal = None +c_update_pk = None +c_output_bief = None + +########################## +# Binding initialisation # +########################## + + +def init_clib(lib_path): + global bief_lib + bief_lib = cdll.LoadLibrary(lib_path) + + init_c_init_bief_from_geo_file(bief_lib) + init_c_get_nb_sections(bief_lib) + init_c_get_nb_points_section(bief_lib) + init_c_set_bief_name(bief_lib) + init_c_st_to_m_compl(bief_lib) + init_c_interpolate_profils_pas_transversal(bief_lib) + init_c_update_pk(bief_lib) + init_c_output_bief(bief_lib) + + +def init_c_init_bief_from_geo_file(bief_lib): + global c_init_bief_from_geo_file + + c_init_bief_from_geo_file = getattr( + bief_lib, 'c_init_bief_from_geo_file' + ) + c_init_bief_from_geo_file.argtypes = [ + c_char_p, POINTER(c_int), POINTER(c_int) + ] + c_init_bief_from_geo_file.restype = None + + +def init_c_get_nb_sections(bief_lib): + global c_get_nb_sections + + c_get_nb_sections = getattr(bief_lib, 'c_get_nb_sections') + c_get_nb_sections.argtypes = [POINTER(c_int)] + c_get_nb_sections.restype = None + + +def init_c_get_nb_points_section(bief_lib): + global c_get_nb_points_section + + c_get_nb_points_section = getattr(bief_lib, 'c_get_nb_points_section') + c_get_nb_points_section.argtypes = [POINTER(c_int), POINTER(c_int)] + c_get_nb_points_section.restype = None + + +def init_c_set_bief_name(bief_lib): + global c_set_bief_name + + c_set_bief_name = getattr(bief_lib, 'c_set_bief_name') + c_set_bief_name.argtypes = [c_char_p] + c_set_bief_name.restype = None + + +def init_c_st_to_m_compl(bief_lib): + global c_st_to_m_compl + + c_st_to_m_compl = getattr(bief_lib, 'c_st_to_m_compl') + c_st_to_m_compl.argtypes = [POINTER(c_int), c_char_p, c_char_p] + c_st_to_m_compl.restype = None + + +def init_c_interpolate_profils_pas_transversal(bief_lib): + global c_interpolate_profils_pas_transversal + + c_interpolate_profils_pas_transversal = getattr( + bief_lib, 'c_interpolate_profils_pas_transversal' + ) + c_interpolate_profils_pas_transversal.argtypes = [ + POINTER(c_int), POINTER(c_int), + c_char_p, c_char_p, + POINTER(c_double), POINTER(c_bool), + POINTER(c_int), POINTER(c_bool) + ] + c_interpolate_profils_pas_transversal.restype = None + + +def init_c_update_pk(bief_lib): + global c_update_pk + + c_update_pk = getattr(bief_lib, 'c_update_pk') + c_update_pk.argtypes = [c_char_p] + c_update_pk.restype = None + + +def init_c_output_bief(bief_lib): + global c_output_bief + + c_output_bief = getattr(bief_lib, 'c_output_bief') + c_output_bief.argtypes = [c_char_p] + c_output_bief.restype = None + + +def init_bief_from_geo_file(name, with_charriage, with_water): + logger.info("! call init_bief_from_geo_file:") + cname = create_string_buffer(name.encode()) + c_init_bief_from_geo_file( + cname, + byref(c_int(with_charriage)), + byref(c_int(with_water)) + ) + +##################### +# Binding functions # +##################### + + +def get_nb_sections(): + nb_sections = c_int(0) + c_get_nb_sections(byref(nb_sections)) + return nb_sections.value + + +def get_nb_points_section(section): + nb_points = c_int(0) + c_get_nb_points_section(byref(c_int(section)), byref(nb_points)) + return nb_points.value + + +def set_bief_name(name): + logger.info(f"! call set_bief_name: {repr(name)}") + cname = create_string_buffer(name.encode()) + c_set_bief_name(cname) + + +def st_to_m_compl(npoints, tag1=' ', tag2=' '): + logger.info(f"! call st_to_m_compl: {npoints}") + cnpoints = c_int(npoints) + ctag1 = create_string_buffer(tag1.encode()) + ctag2 = create_string_buffer(tag2.encode()) + + c_st_to_m_compl(byref(cnpoints), ctag1, ctag2) + + +def interpolate_profils_pas_transversal(limite1, limite2, + directrice1, directrice2, + pas, lplan=False, + lm=3, lineaire=False): + logger.info("! call interpolate_profils_pas_transversal:") + climite1 = c_int(limite1) + climite2 = c_int(limite2) + cpas = c_double(pas) + clplan = c_bool(lplan) + clm = c_int(lm) + clineaire = c_bool(lineaire) + cdirectrice1 = create_string_buffer(directrice1.encode()) + cdirectrice2 = create_string_buffer(directrice2.encode()) + + c_interpolate_profils_pas_transversal( + byref(climite1), byref(climite2), + cdirectrice1, cdirectrice2, + byref(cpas), byref(clplan), + byref(clm), byref(clineaire) + ) + + +def update_pk(directrice): + logger.info("! call update_pk:") + cdirectrice = create_string_buffer(directrice.encode()) + c_update_pk(cdirectrice) + + +def output_bief(name): + logger.info("! call output_bief:") + cname = create_string_buffer(name.encode()) + c_output_bief(cname) + + +########## +# Script # +########## + + +class MageMesh(AScript): + name = "MageMesh" + description = "Mesh ST file to M" + + def usage(self): + logger.info( + f"Usage : {self._args[0]} {self._args[1]} " + + " " + ) + + def run(self): + if len(self._args) < 5: + return 1 + + try: + so = self._args[2] + st = self._args[3] + step = float(self._args[4]) + except Exception as e: + logger.error(f"Argument format error: {e}") + return 2 + + try: + init_clib(so) + except Exception as e: + logger.error(f"Bindings failed: {e}") + return 3 + + self.meshing(st, step) + + return 0 + + def meshing(self, st_file: str, step: float): + workdir = self.get_workdir(st_file) + file_name = st_file.rsplit(".", 1)[0] + reach_name = os.path.basename(file_name) + + # Open + init_bief_from_geo_file(st_file, 0, 0) + set_bief_name(reach_name) + + ns = get_nb_sections() + npts_max = max( + map( + lambda i: get_nb_points_section(i), + range(1, ns) + ) + ) + + # Transform + st_to_m_compl(npts_max, 'rg', 'rd') + interpolate_profils_pas_transversal( + 1, ns, + 'un', 'np', + step + ) + update_pk("un") + + # Save + logger.info(f"Saved meshing geometry to {file_name}.M") + output_bief(f"{file_name}.M") + + def get_workdir(self, file): + workdir = os.path.abspath( + os.path.dirname(file) + ) + + os.makedirs(workdir, exist_ok=True) + logger.info(f"Set working dir to {workdir}") + + return workdir diff --git a/src/Scripts/P3DST.py b/src/Scripts/P3DST.py new file mode 100644 index 00000000..62d0f88f --- /dev/null +++ b/src/Scripts/P3DST.py @@ -0,0 +1,116 @@ +# P3DST.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import sys +import logging + +from matplotlib import pyplot as plt +from numpy import mean + +from Scripts.AScript import AScript + +from Model.Saved import SavedStatus +from Model.Geometry.Reach import Reach + +logger = logging.getLogger() + + +class Script3DST(AScript): + name = "3DST" + description = "Display a 3D plot of a river reach from ST file" + + def usage(self): + logger.info(f"Usage : {self._args[0]} 3DST ") + + def set_axes_equal(self, ax): + """Make axes of 3D plot have equal scale + + Make axes of 3D plot have equal scale so that spheres appear + as spheres, cubes as cubes, etc.. This is one possible + solution to Matplotlib's ax.set_aspect('equal') and + ax.axis('equal') not working for 3D. + + Args: + ax: a matplotlib axis, e.g., as output from plt.gca() + + Returns: + The input axis + """ + x_limits = ax.get_xlim3d() + y_limits = ax.get_ylim3d() + z_limits = ax.get_zlim3d() + + x_range = abs(x_limits[1] - x_limits[0]) + x_middle = mean(x_limits) + y_range = abs(y_limits[1] - y_limits[0]) + y_middle = mean(y_limits) + z_range = abs(z_limits[1] - z_limits[0]) + z_middle = mean(z_limits) + + # The plot bounding box is a sphere in the sense of the infinity + # norm, hence I call half the max range the plot radius. + plot_radius = 0.5*max([x_range, y_range, z_range]) + + ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) + ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) + ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) + + return ax + + def run(self): + try: + st_file = self._args[2] + logger.info(f"Use ST file: {st_file}") + except Exception as e: + logger.error(f"Arguments parcing: {str(e)}") + return 1 + + try: + status = SavedStatus() + + my_reach = Reach(status=status) + my_reach.import_geometry(st_file) + my_reach.compute_guidelines() + + ax = plt.figure().add_subplot(projection="3d") + logger.info(my_reach.get_x()) + for x, y, z in zip( + my_reach.get_x(), + my_reach.get_y(), + my_reach.get_z() + ): + ax.plot(x, y, z, color='r', lw=1.) + + for x, y, z in zip( + my_reach.get_guidelines_x(), + my_reach.get_guidelines_y(), + my_reach.get_guidelines_z() + ): + ax.plot(x, y, z, color='b', lw=1.) + + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_zlabel('Z') + plt.tight_layout() + self.set_axes_equal(ax) + plt.show() + + return 0 + except Exception as e: + logger.error(str(e)) + return 1 diff --git a/src/Scripts/Run.py b/src/Scripts/Run.py new file mode 100644 index 00000000..7b70a910 --- /dev/null +++ b/src/Scripts/Run.py @@ -0,0 +1,113 @@ +# Run.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import logging + +from queue import Queue + +from PyQt5.QtCore import QProcess + +from Scripts.AScript import AScript +from Model.Study import Study + +logger = logging.getLogger() + + +class ScriptRun(AScript): + name = "Run" + description = "Run solver on Pamhyr2 a study" + + def usage(self): + logger.info( + f"Usage : {self._args[0]} {self._args[1]} " + + " " + ) + + def run(self): + if len(self._args) < 4: + return 1 + + command = self._args[1] + solver_name = self._args[2] + study_file = os.path.abspath( + self._args[3] + ) + + try: + solver = next( + filter( + lambda solver: solver.name == solver_name, + self._conf.solvers + ) + ) + except Exception as e: + logger.error(f"No solver found: {e}") + return 2 + + study = Study.open(study_file) + + self._solver = solver + self._study = study + + logger.info( + f"Run {solver.name} ({solver.type}) " + + "on study '{study.name}' ({study_file})" + ) + + # Workdir + workdir = os.path.join( + os.path.dirname(study.filename), + "_PAMHYR_", + study.name.replace(" ", "_"), + solver.name.replace(" ", "_") + ) + os.makedirs(workdir, exist_ok=True) + logger.info(f"Set working dir to {workdir}") + + # Preparate process + p = QProcess(None) + p.setWorkingDirectory(workdir) + + self._q = Queue() + + # Export and Run + logger.info(f"~Export~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + solver.export(study, workdir, qlog=self._q) + + while self._q.qsize() != 0: + s = self._q.get() + logger.info(s) + + if command == "run": + logger.info(f"~Run~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + solver.run( + study, + process=p, + output_queue=self._q + ) + p.waitForFinished() + + logger.info(f"~End~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + + return 0 + + +class ScriptExport(ScriptRun): + name = "Export" + description = "Export Pamhyr2 study for solver" diff --git a/src/Scripts/mage_to_mascaret.py b/src/Scripts/mage_to_mascaret.py new file mode 100755 index 00000000..cbc40325 --- /dev/null +++ b/src/Scripts/mage_to_mascaret.py @@ -0,0 +1,341 @@ +#!/bin/env python3 + +import sys +import importlib.util as imp +import os +import datetime as dt +import lxml.etree as ET + +def mage_to_mascaret(filename): + # rep file + with open(filename, encoding = "ISO-8859-1", mode ="r") as rep: + lines = rep.readlines() + for line in lines: + l = line.split() + if (l[0] == 'PAR'): + parfile = l[1] + elif (l[0] == 'NET'): + netfile = l[1] + elif (l[0] == 'HYD'): + hydfile = l[1] + elif (l[0] == 'RUG'): + rugfile = l[1] + elif (l[0] == 'LIM'): + limfile = l[1] + elif (l[0] == 'INI'): + inifile = l[1] + + # net file + with open(netfile, encoding = "ISO-8859-1", mode ="r") as net: + lines = net.readlines() + for line in lines: + if (line[0] != '*' and line[0] != '$'): + l = line.split() + biefname = l[0] + stfile = l[3] + break # on n'utilise que le premier bief pour le moment + + + # geometry file + + print("geometrie fie : "+stfile) + wb.init_bief_from_geo_file(stfile, 0, 0) + wb.set_bief_name(biefname) + nb_sect = wb.get_nb_sections() + print("nb sections: "+str(nb_sect)) + + # hydrogramme file + with open(hydfile, encoding = "ISO-8859-1", mode ="r") as hyd: + HYD = [] + lines = hyd.readlines() + for line in lines: + if (line[0] != '*' and line[0] != '$'): + HYD.append([x for x in line.split()]) + + # friction file + with open(rugfile, encoding = "ISO-8859-1", mode ="r") as lim: + RUG = [] + lines = lim.readlines() + for line in lines: + if (line[0] != '*' and line[0] != '$'): + RUG.append([x for x in line.split()]) + + # tarage file + with open(limfile, encoding = "ISO-8859-1", mode ="r") as tar: + LIM = [] + lines = tar.readlines() + for line in lines: + if (line[0] != '*' and line[0] != '$'): + LIM.append([x for x in line.split()]) + + # cond init file + with open(inifile, encoding = "ISO-8859-1", mode ="r") as ini: + INI = [] + lines = ini.readlines() + for line in lines: + if (line[0] != '*' and line[0] != '$'): + INI.append([x for x in line.split()]) + + # PAR file + with open(parfile, encoding = "ISO-8859-1", mode ="r") as par: + PAR = {} + lines = par.readlines() + for line in lines: + l = line.split() + if l[0] == 'init_time': + t = l[1].split(":") + if len(t)>1: + PAR['init_time'] = int(t[0])*24*3600+int(t[1])*3600+int(t[2])*60+int(t[3]) + else: + PAR['init_time'] = int(t[0])*60 + elif l[0] == 'final_time': + t = l[1].split(":") + if len(t)>1: + PAR['final_time'] = int(t[0])*24*3600+int(t[1])*3600+int(t[2])*60+int(t[3]) + else: + PAR['final_time'] = int(t[0])*60 + elif l[0] == 'timestep': + PAR['timestep'] = float(l[1]) + elif l[0] == 'timestep_bin': + PAR['timestep_bin'] = float(l[1]) + elif l[0] == 'timestep_tra': + PAR['timestep_tra'] = float(l[1]) + elif l[0] == 'min_height': + PAR['min_height'] = float(l[1]) + + #os.chdir(current_dir) + + with open("hydrogramme.loi", mode ="w") as hyd: + hyd.write(" S\n") + for line in HYD: + hyd.write(" "+line[0]+" "+line[1]+"\n") + if float(HYD[-1][0]) < PAR['final_time']: + hyd.write(" "+str(PAR['final_time'])+" "+str(float(HYD[-1][1]))+"\n") + + with open("limnigramme.loi", mode ="w") as lim: + lim.write(" S\n") + for line in LIM: + lim.write(" "+line[0]+" "+line[1]+"\n") + if float(LIM[-1][0]) < PAR['final_time']: + lim.write(" "+str(PAR['final_time'])+" "+str(float(LIM[-1][1]))+"\n") + + with open("init.lig", mode ="w") as lig: + lig.write("RESULTATS CALCUL,DATE : "+dt.datetime.now().strftime("%d/%m/%y %H:%M"+"\n")) + lig.write("FICHIER RESULTAT MASCARET\n") + lig.write("-"*71+"\n") + lig.write(" IMAX = "+f"{len(INI):4d}"+" NBBIEF= 1\n") + lig.write(" I1,I2 = 1 "+str(len(INI))+"\n") + lig.write(" X\n") + for i in range(int(len(INI)/5)+1): + lig.write(" "+"".join([f"{float(INI[j][4]):12.2f}" for j in range(i*5, min(i*5+5, len(INI)))])+"\n") + lig.write(" Z\n") + for i in range(int(len(INI)/5)+1): + lig.write(" "+"".join([f"{float(INI[j][3]):12.3f}" for j in range(i*5, min(i*5+5, len(INI)))])+"\n") + lig.write(" Q\n") + for i in range(int(len(INI)/5)+1): + lig.write(" "+"".join([f"{float(INI[j][2]):12.3f}" for j in range(i*5, min(i*5+5, len(INI)))])+"\n") + lig.write(" FIN\n") + + #os.remove("geometrie") + wb.output_bief_mascaret("geometrie") + pk = [wb.get_pk_section(i+1) for i in range(nb_sect)] + print(pk) + print(max(pk)) + print(min(pk)) + + out_name = 'FichierCas.xcas' + root = ET.Element('fichierCas') + pc = ET.SubElement(root, 'parametresCas') + pg = ET.SubElement(pc, 'parametresGeneraux') + ET.SubElement(pg, 'versionCode').text='3' + ET.SubElement(pg, 'code').text = '3' + ET.SubElement(pg, 'fichMotsCles').text = out_name + ET.SubElement(pg, 'dictionaire').text='dico.txt' + ET.SubElement(pg, 'progPrincipal').text='princi.f' + ET.SubElement(pg, 'sauveModele').text='false' + ET.SubElement(pg, 'fichSauvModele').text='mascaret_exp.tmp' + ET.SubElement(pg, 'validationCode').text='false' + ET.SubElement(pg, 'typeValidation').text='1' + ET.SubElement(pg, 'presenceCasiers').text='false' + b = ET.SubElement(pg, 'bibliotheques') + ET.SubElement(b, 'bibliotheque').text='mascaretV5P1.a damoV3P0.a' + pmp = ET.SubElement(pc, 'parametresModelePhysique') + ET.SubElement(pmp, 'perteChargeConf').text='false' + ET.SubElement(pmp, 'compositionLits').text='1' + ET.SubElement(pmp, 'conservFrotVertical').text='false' + ET.SubElement(pmp, 'elevCoteArrivFront').text='0.05' + ET.SubElement(pmp, 'interpolLinStrickler').text='false' + d = ET.SubElement(pmp, 'debordement') + ET.SubElement(d, 'litMajeur').text='false' + ET.SubElement(d, 'zoneStock').text='false' + pn = ET.SubElement(pc, 'parametresNumeriques') + ET.SubElement(pn, 'calcOndeSubmersion').text='false' + ET.SubElement(pn, 'decentrement').text='false' + ET.SubElement(pn, 'froudeLimCondLim').text='1000.0' + ET.SubElement(pn, 'traitImplicitFrot').text='false' + ET.SubElement(pn, 'hauteurEauMini').text=str(PAR['min_height']) + ET.SubElement(pn, 'implicitNoyauTrans').text='true' + ET.SubElement(pn, 'optimisNoyauTrans').text='false' + ET.SubElement(pn, 'perteChargeAutoElargissement').text='false' + ET.SubElement(pn, 'termesNonHydrostatiques').text='false' + ET.SubElement(pn, 'apportDebit').text='0' + ET.SubElement(pn, 'attenuationConvection').text='false' + pt = ET.SubElement(pc, 'parametresTemporels') + ET.SubElement(pt, 'pasTemps').text=str(PAR['timestep']) + ET.SubElement(pt, 'tempsInit').text=str(PAR['init_time']) + ET.SubElement(pt, 'critereArret').text='1' + ET.SubElement(pt, 'nbPasTemps').text='2000' + ET.SubElement(pt, 'tempsMax').text=str(PAR['final_time']) + ET.SubElement(pt, 'pasTempsVar').text='true' + ET.SubElement(pt, 'nbCourant').text='0.8' + ET.SubElement(pt, 'coteMax').text='0.0' + ET.SubElement(pt, 'abscisseControle').text='0.0' + ET.SubElement(pt, 'biefControle').text='1' + pgr = ET.SubElement(pc, 'parametresGeometrieReseau') + g = ET.SubElement(pgr, 'geometrie') + ET.SubElement(g, 'fichier').text='geometrie' + ET.SubElement(g, 'format').text='2' + ET.SubElement(g, 'profilsAbscAbsolu').text='true' + lb = ET.SubElement(pgr, 'listeBranches') + ET.SubElement(lb, 'nb').text='1' + ET.SubElement(lb, 'numeros').text='1' + ET.SubElement(lb, 'abscDebut').text=str(min(pk)) + ET.SubElement(lb, 'abscFin').text=str(max(pk)) + ET.SubElement(lb, 'numExtremDebut').text='1' + ET.SubElement(lb, 'numExtremFin').text='2' + ln = ET.SubElement(pgr, 'listeNoeuds') + ET.SubElement(ln, 'nb').text='0' + ET.SubElement(ln, 'noeuds') + el = ET.SubElement(pgr, 'extrLibres') + ET.SubElement(el, 'nb').text='2' + ET.SubElement(el, 'num').text='1 2' + ET.SubElement(el, 'numExtrem').text='1 2' + noms = ET.SubElement(el, 'noms') + ET.SubElement(noms, 'string').text='limite1' + ET.SubElement(noms, 'string').text='limite2' + ET.SubElement(el, 'typeCond').text='1 2' + ET.SubElement(el, 'numLoi').text='1 2' + pc2 = ET.SubElement(pc, 'parametresConfluents') + ET.SubElement(pc2, 'nbConfluents').text='0' + ET.SubElement(pc2, 'confluents') + pcm = ET.SubElement(pc, 'parametresPlanimetrageMaillage') + ET.SubElement(pcm, 'methodeMaillage').text='1' + planim = ET.SubElement(pcm, 'planim') + ET.SubElement(planim, 'nbPas').text='101' + ET.SubElement(planim, 'nbZones').text='1' + ET.SubElement(planim, 'valeursPas').text='0.1' + ET.SubElement(planim, 'num1erProf').text='1' + ET.SubElement(planim, 'numDerProf').text=str(nb_sect) + maillage = ET.SubElement(pcm, 'maillage') + ET.SubElement(maillage, 'modeSaisie').text='2' + ET.SubElement(maillage, 'sauvMaillage').text='false' + maillageClavier = ET.SubElement(maillage, 'maillageClavier') + ET.SubElement(maillageClavier, 'nbSections').text='0' + ET.SubElement(maillageClavier, 'nbPlages').text='1' + ET.SubElement(maillageClavier, 'num1erProfPlage').text='1' + ET.SubElement(maillageClavier, 'numDerProfPlage').text=str(nb_sect) + ET.SubElement(maillageClavier, 'pasEspacePlage').text='10.0' + ET.SubElement(maillageClavier, 'nbZones').text='0' + ps = ET.SubElement(pc, 'parametresSingularite') + ET.SubElement(ps, 'nbSeuils').text='0' + pad = ET.SubElement(pc, 'parametresApportDeversoirs') + pc3 = ET.SubElement(pc, 'parametresCalage') + frt = ET.SubElement(pc3, 'frottement') + ET.SubElement(frt, 'loi').text='1' + ET.SubElement(frt, 'nbZone').text=str(len(RUG)) + ET.SubElement(frt, 'numBranche').text=' '.join([i[1] for i in RUG]) + ET.SubElement(frt, 'absDebZone').text=' '.join([i[2] for i in RUG]) + ET.SubElement(frt, 'absFinZone').text=' '.join([i[3] for i in RUG]) + ET.SubElement(frt, 'coefLitMin').text=' '.join([i[4] for i in RUG]) + ET.SubElement(frt, 'coefLitMaj').text=' '.join([i[5] for i in RUG]) + zst = ET.SubElement(pc3, 'zoneStockage') + ET.SubElement(zst, 'nbProfils').text='0' + ET.SubElement(zst, 'numProfil').text='-0' + ET.SubElement(zst, 'limGauchLitMaj').text='-0' + ET.SubElement(zst, 'limDroitLitMaj').text='-0' + plh = ET.SubElement(pc, 'parametresLoisHydrauliques') + ET.SubElement(plh, 'nb').text='2' + lois = ET.SubElement(plh, 'lois') + spl = ET.SubElement(lois, 'structureParametresLoi') + ET.SubElement(spl, 'nom').text='loi_1_hydrogramme' + ET.SubElement(spl, 'type').text='1' + donnees = ET.SubElement(spl, 'donnees') + ET.SubElement(donnees, 'modeEntree').text='1' + ET.SubElement(donnees, 'fichier').text='hydrogramme.loi' + ET.SubElement(donnees, 'uniteTps').text='-0' + ET.SubElement(donnees, 'nbPoints').text='-0' + ET.SubElement(donnees, 'nbDebitsDifferents').text='-0' + spl2 = ET.SubElement(lois, 'structureParametresLoi') + ET.SubElement(spl2, 'nom').text='loi_2_limnigramme' + ET.SubElement(spl2, 'type').text='2' + donnees = ET.SubElement(spl2, 'donnees') + ET.SubElement(donnees, 'modeEntree').text='1' + ET.SubElement(donnees, 'fichier').text='limnigramme.loi' + ET.SubElement(donnees, 'uniteTps').text='-0' + ET.SubElement(donnees, 'nbPoints').text='-0' + ET.SubElement(donnees, 'nbDebitsDifferents').text='-0' + pci = ET.SubElement(pc, 'parametresConditionsInitiales') + re = ET.SubElement(pci, 'repriseEtude') + ET.SubElement(re, 'repriseCalcul').text='false' + le = ET.SubElement(pci, 'ligneEau') + ET.SubElement(le, 'LigEauInit').text='true' + ET.SubElement(le, 'modeEntree').text='1' + ET.SubElement(le, 'fichLigEau').text='init.lig' + ET.SubElement(le, 'formatFichLig').text='2' + ET.SubElement(le, 'nbPts').text='-0' + pir = ET.SubElement(pc, 'parametresImpressionResultats') + ET.SubElement(pir, 'titreCalcul').text='Etude hydraulique1d' + impression = ET.SubElement(pir, 'impression') + ET.SubElement(impression, 'impressionGeometrie').text='false' + ET.SubElement(impression, 'impressionPlanimetrage').text='false' + ET.SubElement(impression, 'impressionReseau').text='false' + ET.SubElement(impression, 'impressionLoiHydraulique').text='false' + ET.SubElement(impression, 'impressionligneEauInitiale').text='false' + ET.SubElement(impression, 'impressionCalcul').text='false' + ps = ET.SubElement(pir, 'pasStockage') + ET.SubElement(ps, 'premPasTpsStock').text='1' + ET.SubElement(ps, 'pasStock').text=str(int(PAR['timestep_bin']/PAR['timestep'])) + ET.SubElement(ps, 'pasImpression').text=str(int(PAR['timestep_tra']/PAR['timestep'])) + results = ET.SubElement(pir, 'resultats') + ET.SubElement(results, 'fichResultat').text='mascaret_exp.opt' + ET.SubElement(results, 'postProcesseur').text='2' + listing = ET.SubElement(pir, 'listing') + ET.SubElement(listing, 'fichListing').text='mascaret_exp.lis' + reprise = ET.SubElement(pir, 'fichReprise') + ET.SubElement(reprise, 'fichRepriseEcr').text='mascaret_exp.rep' + rub = ET.SubElement(pir, 'rubens') + ET.SubElement(rub, 'ecartInterBranch').text='1.0' + stockage = ET.SubElement(pir, 'stockage') + ET.SubElement(stockage, 'option').text='1' + ET.SubElement(stockage, 'nbSite').text='0' + pvc = ET.SubElement(pc, 'parametresVariablesCalculees') + ET.SubElement(pvc, 'variablesCalculees').text="false false false false false false false false false false false false false false false" + + pvs = ET.SubElement(pc, 'parametresVariablesStockees') + ET.SubElement(pvs, 'variablesStockees').text="true false false false false true true true false false false false false false false false false false false true false false true false false false false false false false false false false false false false false false false false false false" + + tree = ET.ElementTree(root) + xmlstr = ET.tostring(root, encoding='unicode', pretty_print=True) + tree.write(out_name) + print("fin du programme") + + parser = ET.XMLParser(remove_blank_text=True) + tree = ET.parse(out_name, parser) + tree.write(out_name, pretty_print=True) + + +if __name__ == "__main__": + + current_dir = os.getcwd() + os.chdir('/home/theophile.terraz/Codes/mage/src') + spec = imp.spec_from_file_location("wb", "./wrapper_bief.py") + wb = imp.module_from_spec(spec) + spec.loader.exec_module(wb) + os.chdir(current_dir) + + print( 'Number of arguments:', len(sys.argv), 'arguments.') + print( 'Argument List:', str(sys.argv)) + mage_to_mascaret(sys.argv[1]) + + + diff --git a/src/Scripts/mascaret_to_mage.py b/src/Scripts/mascaret_to_mage.py new file mode 100755 index 00000000..9942132b --- /dev/null +++ b/src/Scripts/mascaret_to_mage.py @@ -0,0 +1,99 @@ +#!/bin/env python3 + +import sys +import importlib.util as imp +import os +import numpy as np + +def lire_opt(filename, res): + with open(filename, encoding = "ISO-8859-1", mode ="r") as f: + variables = ['"t"','"Bief"','"Section"','"Pk"'] + lines = f.readlines() + for start,line in enumerate(lines): + print("line n "+str(start)+" : ",line) + if line == "[variables]\n": continue + if line == "[resultats]\n": break + val = line.split(';') + variables.append(val[1]) + if val[1] == '"ZREF"': izref = start+3 + if val[1] == '"Q"': iq = start+3 + if val[1] == '"Z"': iz = start+3 + print ("variables : "+str(variables)) + + #resultats + res.mage_version = 83 + # first pass to count sect and biefs + prev_timestep = float(lines[start+1].split(';')[0]) + for line in lines[start+1:]: + ls = line.split(';') + t = float(ls[0]) + if prev_timestep != t : break + ibief = int(ls[1].strip('"')) + isect = int(ls[2].strip('"')) + pk = float(ls[3]) + if ibief > res.ibmax: + res.ibmax += 1 + res.is1.append(isect) + res.is2.append(isect) + if isect > res.ismax: + res.ismax += 1 + res.xgeo.append(pk) + res.ygeo.append(0.0) + res.zfd.append(float(ls[izref])) + res.ybas.append(0.0) + if isect > res.is2[ibief-1]: + res.is2[ibief-1] = isect + if isect < res.is1[ibief-1]: + res.is1[ibief-1] = isect + + print(" nb sections : "+str(res.ismax)) + + # first pass to get the data + prev_bief = 0 + prev_sect = 0 + z = np.zeros(res.ismax, dtype=np.float32) + q = np.zeros(res.ismax, dtype=np.float32) + isect = 0 + count = np.array([1],dtype=np.int32) + bZ = bytearray('Z'.encode()) + bQ = bytearray('Q'.encode()) + for line in lines[start+1:]: + ls = line.split(';') + t = float(ls[0]) + ibief = int(ls[1].strip('"')) + isect = int(ls[2].strip('"')) + if prev_timestep != t : + res.values['Z'].append(prev_timestep, z) + res.values['Q'].append(prev_timestep, q) + count[0] = 4 + 8 + 1 + 8*len(z) + res.raw_data.append(count) + res.raw_data.append(np.array([len(z)],dtype=np.int32)) + res.raw_data.append(np.array([prev_timestep],dtype=np.float64)) + res.raw_data.append(np.array(bZ,dtype=np.byte)) + res.raw_data.append(z) + res.raw_data.append(count) + res.raw_data.append(count) + res.raw_data.append(np.array([len(q)],dtype=np.int32)) + res.raw_data.append(np.array([prev_timestep],dtype=np.float64)) + res.raw_data.append(np.array(bQ,dtype=np.byte)) + res.raw_data.append(q) + res.raw_data.append(count) + prev_timestep = t + z[isect-1] = float(ls[iz]) + q[isect-1] = float(ls[iq]) + + +if __name__ == "__main__": + + current_dir = os.getcwd() + os.chdir('/home/theophile.terraz/Codes/mage/src') + spec = imp.spec_from_file_location("me", "./mage_extraire.py") + me = imp.module_from_spec(spec) + spec.loader.exec_module(me) + os.chdir(current_dir) + + print( 'Number of arguments:', len(sys.argv), 'arguments.') + print( 'Argument List:', str(sys.argv)) + res = me.data() + lire_opt(sys.argv[1], res) + res.write_bin_8(sys.argv[2][0:-4]) diff --git a/src/Scripts/plot_3DST.py b/src/Scripts/plot_3DST.py deleted file mode 100644 index 41c7fb07..00000000 --- a/src/Scripts/plot_3DST.py +++ /dev/null @@ -1,68 +0,0 @@ -# plot_3DST.py -- Pamhyr -# Copyright (C) 2023 INRAE -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# -*- coding: utf-8 -*- - -# a lancer depuis src -import sys -from matplotlib import pyplot as plt -from Model.Geometry.Reach import Reach -from numpy import mean - -def set_axes_equal(ax): - '''Make axes of 3D plot have equal scale so that spheres appear as spheres, - cubes as cubes, etc.. This is one possible solution to Matplotlib's - ax.set_aspect('equal') and ax.axis('equal') not working for 3D. - - Input - ax: a matplotlib axis, e.g., as output from plt.gca(). - ''' - - x_limits = ax.get_xlim3d() - y_limits = ax.get_ylim3d() - z_limits = ax.get_zlim3d() - - x_range = abs(x_limits[1] - x_limits[0]) - x_middle = mean(x_limits) - y_range = abs(y_limits[1] - y_limits[0]) - y_middle = mean(y_limits) - z_range = abs(z_limits[1] - z_limits[0]) - z_middle = mean(z_limits) - - # The plot bounding box is a sphere in the sense of the infinity - # norm, hence I call half the max range the plot radius. - plot_radius = 0.5*max([x_range, y_range, z_range]) - - ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) - ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) - ax.set_zlim3d([z_middle - 0.1*plot_radius, z_middle + 0.1*plot_radius]) - -st_file = sys.argv[1] -my_reach = Reach(None) -my_reach.import_geometry(st_file) -my_reach.compute_guidelines() - -ax = plt.figure().add_subplot(projection="3d") -for x, y, z in zip(my_reach.get_x(), my_reach.get_y(), my_reach.get_z()): - ax.plot(x, y, z, color='r', lw=1.) -for x, y, z in zip(my_reach.get_guidelines_x(), my_reach.get_guidelines_y(), my_reach.get_guidelines_z()): - ax.plot(x, y, z, color='b', lw=1.) -ax.set_xlabel('X') -ax.set_ylabel('Y') -ax.set_zlabel('Z') -plt.tight_layout() -set_axes_equal(ax) -plt.show() diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index d71f0472..5ff5f6dc 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -25,7 +25,7 @@ try: # Installation allow Unix-like signal from signal import SIGTERM, SIGSTOP, SIGCONT _signal = True -except: +except Exception: _signal = False from enum import Enum @@ -37,12 +37,14 @@ from Model.Results.River.River import River, Reach, Profile logger = logging.getLogger() + class STATUS(Enum): NOT_LAUNCHED = -1 STOPED = 0 RUNNING = 1 PAUSED = 5 + class AbstractSolver(object): _type = "" @@ -57,17 +59,6 @@ class AbstractSolver(object): self._name = name self._description = "" - self._path_input = "" - self._path_solver = "" - self._path_output = "" - - self._cmd_input = "" - self._cmd_solver = "" - self._cmd_output = "" - - self._process = None - self._output = None - # Last study running self._study = None @@ -90,7 +81,6 @@ class AbstractSolver(object): ("all_init_time", "000:00:00:00"), ("all_final_time", "999:99:00:00"), ("all_timestep", "300.0"), - ("all_command_line_arguments", ""), ] return lst @@ -131,6 +121,9 @@ class AbstractSolver(object): def is_stoped(self): return self._status == STATUS.STOPED + def has_results_loaded(self): + self._status = STATUS.NOT_LAUNCHED + @name.setter def name(self, name): self._name = name @@ -139,249 +132,37 @@ class AbstractSolver(object): def description(self, description): self._description = description - def set_input(self, path, cmd): - self._path_input = path - self._cmd_input = cmd - - def set_solver(self, path, cmd): - self._path_solver = path - self._cmd_solver = cmd - - def set_output(self, path, cmd): - self._path_output = path - self._cmd_output = cmd - ########## # Export # ########## - def export(self, study, repertory, qlog = None): + def export(self, study, repertory, qlog=None): raise NotImplementedMethodeError(self, self.export) - def cmd_args(self, study): - """Return solver command line arguments list - - Returns: - Command line arguments list - """ - params = study.river.get_params(self.type) - args = params.get_by_key("all_command_line_arguments") - - return args.split(" ") - - def input_param(self): - """Return input command line parameter(s) - - Returns: - Returns input parameter(s) string - """ - raise NotImplementedMethodeError(self, self.input_param) - - def log_file(self): - """Return log file name - - Returns: - Returns log file name as string - """ - raise NotImplementedMethodeError(self, self.log_file) - ########### # RESULTS # ########### @timer - def results(self, study, repertory, qlog = None): - results = Results(study = study) + def results(self, study, repertory, qlog=None): + results = Results(study=study) return results ####### # Run # ####### - def _install_dir(self): - return os.path.abspath( - os.path.join( - os.path.dirname(__file__), - "..", ".." - ) - ) - - def _format_command(self, study, cmd, path = ""): - """Format command line - - Args: - cmd: The command line - path: Optional path string (replace @path in cmd) - - Returns: - The executable and list of arguments - """ - # HACK: Works in most case... Trust me i'm an engineer - cmd = cmd.replace("@install_dir", self._install_dir()) - cmd = cmd.replace("@path", path.replace(" ", "\ ")) - cmd = cmd.replace("@input", self.input_param()) - cmd = cmd.replace("@dir", self._process.workingDirectory()) - cmd = cmd.replace("@args", " ".join(self.cmd_args(study))) - - logger.debug(f"! {cmd}") - - if cmd[0] == "\"": - # Command line executable path is between " char - cmd = cmd.split("\"") - exe = cmd[1].replace("\ ", " ") - args = list( - filter( - lambda s: s != "", - "\"".join(cmd[2:]).split(" ")[1:] - ) - ) - else: - # We suppose the command line executable path as no space char - cmd = cmd.replace("\ ", "&_&").split(" ") - exe = cmd[0].replace("&_&", " ") - args = list( - filter( - lambda s: s != "", - map(lambda s: s.replace("&_&", "\ "), cmd[1:]) - ) - ) - - logger.info(f"! {exe} {args}") - return exe, args - - def run_input_data_fomater(self, study): - if self._cmd_input == "": - self._run_next(study) - return True - - cmd = self._cmd_input - exe, args = self._format_command(study, cmd, self._path_input) - - if not os.path.exists(exe): - error = f"[ERROR] Path {exe} do not exists" - logger.info(error) - return error - - self._process.start( - exe, args, - ) - - return True - - def run_solver(self, study): - if self._cmd_solver == "": - self._run_next(study) - return True - - cmd = self._cmd_solver - exe, args = self._format_command(study, cmd, self._path_solver) - - if not os.path.exists(exe): - error = f"[ERROR] Path {exe} do not exists" - logger.info(error) - return error - - self._process.start( - exe, args, - ) - - self._status = STATUS.RUNNING - return True - - def run_output_data_fomater(self, study): - if self._cmd_output == "": - self._run_next(study) - return True - - cmd = self._cmd_output - exe, args = self._format_command(study, cmd, self._path_output) - - if not os.path.exists(exe): - error = f"[ERROR] Path {exe} do not exists" - logger.info(error) - return error - - self._process.start( - exe, args, - ) - - return True - - def _data_ready(self): - s = self._process.readAll().data().decode() - if self._output is not None: - for x in s.split('\n'): - self._output.put(x) - - def _run_next(self, study): - self._step += 1 - if self._step < len(self._runs): - res = self._runs[self._step](study) - if res is not True: - self._output.put(res) - else: - self._status = STATUS.STOPED - - def _finished(self, study, exit_code, exit_status): - if self._output is not None: - self._output.put(exit_code) - - self._run_next(study) - - def run(self, study, process = None, output_queue = None): - if process is not None: - self._process = process - if output_queue is not None: - self._output = output_queue - - self._process.readyRead.connect(self._data_ready) - self._process.finished.connect(lambda c, s: self._finished(study, c, s)) - - self._runs = [ - self.run_input_data_fomater, - self.run_solver, - self.run_output_data_fomater, - ] - self._step = 0 - # Run first step - res = self._runs[0](study) - if res is not True: - self._output.put(res) + def run(self, study): + raise NotImplementedMethodeError(self, self.run) def kill(self): - if self._process is None: - return True + raise NotImplementedMethodeError(self, self.kill) - self._process.kill() - self._status = STATUS.STOPED - return True - - def start(self, study, process = None): - if _signal: - # Solver is PAUSED, so continue execution - if self._status == STATUS.PAUSED: - os.kill(self._process.pid(), SIGCONT) - self._status = STATUS.RUNNING - return True - - self.run(study, process) - return True + def start(self, study, process=None): + raise NotImplementedMethodeError(self, self.start) def pause(self): - if _signal: - if self._process is None: - return False - - # Send SIGSTOP to PAUSED solver - os.kill(self._process.pid(), SIGSTOP) - self._status = STATUS.PAUSED - return True - return False + raise NotImplementedMethodeError(self, self.pause) def stop(self): - if self._process is None: - return False - - self._process.terminate() - self._status = STATUS.STOPED - return True + raise NotImplementedMethodeError(self, self.stop) diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py new file mode 100644 index 00000000..2a6d9ec8 --- /dev/null +++ b/src/Solver/CommandLine.py @@ -0,0 +1,324 @@ +# CommandLine.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import logging + +from tools import timer, parse_command_line + +try: + # Installation allow Unix-like signal + from signal import SIGTERM, SIGSTOP, SIGCONT + _signal = True +except Exception: + _signal = False + +from enum import Enum + +from Model.Except import NotImplementedMethodeError + +from Model.Results.Results import Results +from Model.Results.River.River import River, Reach, Profile + +from Solver.ASolver import AbstractSolver, STATUS + +logger = logging.getLogger() + + +class CommandLineSolver(AbstractSolver): + _type = "" + + def __init__(self, name): + super(CommandLineSolver, self).__init__(name) + + self._current_process = None + self._status = STATUS.NOT_LAUNCHED + + self._path_input = "" + self._path_solver = "" + self._path_output = "" + + self._cmd_input = "" + self._cmd_solver = "" + self._cmd_output = "" + + self._process = None + self._output = None + + # Last study running + self._study = None + + @classmethod + def default_parameters(cls): + lst = super(CommandLineSolver, cls).default_parameters() + + lst += [ + ("all_command_line_arguments", ""), + ] + + return lst + + def set_input(self, path, cmd): + self._path_input = path + self._cmd_input = cmd + + def set_solver(self, path, cmd): + self._path_solver = path + self._cmd_solver = cmd + + def set_output(self, path, cmd): + self._path_output = path + self._cmd_output = cmd + + ########## + # Export # + ########## + + def cmd_args(self, study): + """Return solver command line arguments list + + Returns: + Command line arguments list + """ + params = study.river.get_params(self.type) + args = params.get_by_key("all_command_line_arguments") + + return args.split(" ") + + def input_param(self): + """Return input command line parameter(s) + + Returns: + Returns input parameter(s) string + """ + raise NotImplementedMethodeError(self, self.input_param) + + def output_param(self): + """Return output command line parameter(s) + + Returns: + Returns output parameter(s) string + """ + raise NotImplementedMethodeError(self, self.output_param) + + def log_file(self): + """Return log file name + + Returns: + Returns log file name as string + """ + raise NotImplementedMethodeError(self, self.log_file) + + ####### + # Run # + ####### + + def _install_dir(self): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", ".." + ) + ) + + def _format_command(self, study, cmd, path=""): + """Format command line + + Args: + cmd: The command line + path: Optional path string (replace @path in cmd) + + Returns: + The executable and list of arguments + """ + cmd = cmd.replace("@install_dir", self._install_dir()) + cmd = cmd.replace("@path", "\"" + path + "\"") + cmd = cmd.replace("@input", self.input_param()) + cmd = cmd.replace("@output", self.output_param()) + cmd = cmd.replace("@dir", self._process.workingDirectory()) + cmd = cmd.replace("@args", " ".join(self.cmd_args(study))) + + logger.debug(f"! {cmd}") + + words = parse_command_line(cmd) + exe = words[0] + args = words[1:] + + logger.info(f"! {exe} {args}") + return exe, args + + def run_input_data_fomater(self, study): + if self._cmd_input == "": + self._run_next(study) + return True + + cmd = self._cmd_input + exe, args = self._format_command(study, cmd, self._path_input) + + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.warning(error) + return error + + self._process.start( + exe, args, + ) + self._process.waitForStarted() + + return True + + def run_solver(self, study): + if self._cmd_solver == "": + self._run_next(study) + return True + + cmd = self._cmd_solver + exe, args = self._format_command(study, cmd, self._path_solver) + + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.warning(error) + return error + + self._process.start( + exe, args, + ) + self._process.waitForStarted() + + self._status = STATUS.RUNNING + return True + + def run_output_data_fomater(self, study): + if self._cmd_output == "": + self._run_next(study) + return True + + cmd = self._cmd_output + exe, args = self._format_command(study, cmd, self._path_output) + + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.warning(error) + return error + + self._process.start( + exe, args, + ) + self._process.waitForStarted() + + return True + + def _data_ready(self): + # Read process output and put lines in queue + s = self._process.readAll().data().decode() + + if self._output is not None: + for x in s.split('\n'): + self._output.put(x) + + def _finished(self, study, exit_code, exit_status): + if self._output is not None: + self._output.put(exit_code) + + logger.debug( + "Process finished with " + + f"code: {exit_code}, status: {exit_status}" + ) + + self._run_next(study) + + def _run_next(self, study): + self._step += 1 + + if self._step >= len(self._runs): + self._status = STATUS.STOPED + return + + fn = self._runs[self._step] + res = fn(study) + + if res is not True: + self._output.put(res) + + def run(self, study, process=None, output_queue=None): + self._study = study + + # Replace old values if needed + if process is not None: + self._process = process + if output_queue is not None: + self._output = output_queue + + # Connect / reconnect signal + self._process.readyRead.connect(self._data_ready) + self._process.finished.connect( + lambda c, s: self._finished(study, c, s) + ) + + # Prepare running step + self._runs = [ + self.run_input_data_fomater, + self.run_solver, + self.run_output_data_fomater, + ] + self._step = 0 + + self._status = STATUS.RUNNING + + # Run first step + res = self._runs[0](study) + if res is not True: + self._output.put(res) + + def kill(self): + if self._process is None: + return True + + self._process.kill() + self._status = STATUS.STOPED + return True + + def start(self, study, process=None): + if _signal: + # Solver is PAUSED, so continue execution + if self._status == STATUS.PAUSED: + os.kill(self._process.pid(), SIGCONT) + self._status = STATUS.RUNNING + return True + + self.run(study, process) + return True + + def pause(self): + if _signal: + if self._process is None: + return False + + # Send SIGSTOP to PAUSED solver + os.kill(self._process.pid(), SIGSTOP) + self._status = STATUS.PAUSED + return True + return False + + def stop(self): + if self._process is None: + return False + + self._process.terminate() + self._status = STATUS.STOPED + return True diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py index 9e588bae..f8ff31aa 100644 --- a/src/Solver/GenericSolver.py +++ b/src/Solver/GenericSolver.py @@ -16,11 +16,10 @@ # -*- coding: utf-8 -*- -from Solver.ASolver import ( - AbstractSolver, STATUS -) +from Solver.CommandLine import CommandLineSolver -class GenericSolver(AbstractSolver): + +class GenericSolver(CommandLineSolver): _type = "generic" def __init__(self, name): diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 1cad3f68..00d44098 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -20,9 +20,9 @@ import os import logging import numpy as np -from tools import timer +from tools import timer, trace -from Solver.ASolver import AbstractSolver +from Solver.CommandLine import CommandLineSolver from Checker.Mage import MageNetworkGraphChecker from Model.Results.Results import Results @@ -30,16 +30,18 @@ from Model.Results.River.River import River, Reach, Profile logger = logging.getLogger() + def mage_file_open(filepath, mode): f = open(filepath, mode) if "w" in mode: # Write header - f.write("* This file is generate by PAMHYR, please don't modify\n") + f.write("* This file is generated by PAMHYR, please don't modify\n") return f -class Mage(AbstractSolver): + +class Mage(CommandLineSolver): _type = "mage" def __init__(self, name): @@ -51,7 +53,6 @@ class Mage(AbstractSolver): self._cmd_solver = "@path @input -o @output" self._cmd_output = "" - @classmethod def default_parameters(cls): lst = super(Mage, cls).default_parameters() @@ -80,6 +81,7 @@ class Mage(AbstractSolver): ("mage_compute_reach_volume_balance", "y"), ("mage_max_reach_volume_balance", "0.001"), ("mage_min_reach_volume_to_check", "1000.0"), + ("mage_init_internal", " "), ] return lst @@ -87,8 +89,8 @@ class Mage(AbstractSolver): @classmethod def checkers(cls): lst = [ - MageNetworkGraphChecker(connectivity = True), - MageNetworkGraphChecker(connectivity = False) + MageNetworkGraphChecker(connectivity=True), + MageNetworkGraphChecker(connectivity=False) ] return lst @@ -98,16 +100,20 @@ class Mage(AbstractSolver): ########## def cmd_args(self, study): - l = super(Mage, self).cmd_args(study) + lst = super(Mage, self).cmd_args(study) - l.append("-r") + lst.append("-r") - return l + return lst def input_param(self): name = self._study.name return f"{name}.REP" + def output_param(self): + name = self._study.name + return f"{name}.BIN" + def log_file(self): name = self._study.name return f"{name}.TRA" @@ -120,7 +126,6 @@ class Mage(AbstractSolver): qlog.put("Export ST file") os.makedirs(os.path.join(repertory, "net"), exist_ok=True) - gra_file = f"{name}.GRA" # Write header edges = study.river.edges() @@ -136,55 +141,91 @@ class Mage(AbstractSolver): if edge._name == "": name = f"Reach_{edge.id}" - with mage_file_open(os.path.join(repertory, "net", f"{name}.ST"), "w+") as f: + with mage_file_open( + os.path.join(repertory, "net", f"{name}.ST"), + "w+" + ) as f: files.append(str(os.path.join("net", f"{name}.ST"))) cnt_num = 1 for profile in edge.reach.profiles: - num = f"{cnt_num:>6}" - c1 = f"{profile.code1:>6}" - c2 = f"{profile.code2:>6}" - t = f"{len(profile.points):>6}" - kp = f"{profile.kp:>13.4f}" - pname = profile.name - if profile.name == "": - pname = f"p{profile.id:>3}".replace(" ", "p") - name = f"{pname}" - - sediment = "" - if profile.sl is not None: - if not any(filter(lambda f: ".GRA" in f, files)): - files.append(gra_file) - - nl = len(profile.sl) - sediment = f" {nl:>3}" - for l in profile.sl.layers: - sediment += f" {l.height:>10} {l.d50:>10} {l.sigma:>10} {l.critical_constraint:>10}" - - f.write(f"{num}{c1}{c2}{t}{kp} {name} {sediment}\n") + self._export_ST_profile_header( + f, files, profile, cnt_num + ) cnt_num += 1 + # Points for point in profile.points: - x = f"{point.x:>13.4f}" - y = f"{point.y:>13.4f}" - z = f"{point.z:>13.4f}" - n = f"{point.name:<3}" - - sediment = "" - prev = point.z - if point.sl is not None: - nl = len(point.sl) - sediment = f"{nl:>3}" - for l in point.sl.layers: - prev = round(prev - l.height, 5) - sediment += f" {prev:>10} {l.d50:>10} {l.sigma:>10} {l.critical_constraint:>10}" - - f.write(f"{x}{y}{z} {n} {sediment}\n") + self._export_ST_point_line( + f, files, point + ) + # Profile last line f.write(f" 999.9990 999.9990 999.9990\n") return files + def _export_ST_profile_header(self, wfile, files, + profile, cnt): + num = f"{cnt:>6}" + c1 = f"{profile.code1:>6}" + c2 = f"{profile.code2:>6}" + t = f"{len(profile.points):>6}" + kp = f"{profile.kp:>12f}"[0:12] + pname = profile.name + if profile.name == "": + # Generate name from profile id prefixed with + # 'p' (and replace space char with '0' char) + pname = f"p{profile.id:>3}".replace(" ", "0") + name = f"{pname:<19}" + + # Generate sediment additional data if available + sediment = "" + if profile.sl is not None: + if not any(filter(lambda f: ".GRA" in f, files)): + files.append(self._gra_file) + + # Number of layers + nl = len(profile.sl) + sediment = f" {nl:>3}" + + # Layers data + for layer in profile.sl.layers: + sediment += ( + f" {layer.height:>10} {layer.d50:>10} " + + f"{layer.sigma:>10} " + + f"{layer.critical_constraint:>10}" + ) + + # Profile header line + wfile.write(f"{num}{c1}{c2}{t} {kp} {pname} {sediment}\n") + + def _export_ST_point_line(self, wfile, files, point): + x = f"{point.x:<12.4f}"[0:12] + y = f"{point.y:<12.4f}"[0:12] + z = f"{point.z:<12.4f}"[0:12] + n = f"{point.name:<3}" + + # Generate sediment additional data if available + sediment = "" + prev = point.z + if point.sl is not None: + # Number of layers + nl = len(point.sl) + sediment = f"{nl:>3}" + + # Layers data + for layer in point.sl.layers: + prev = round(prev - layer.height, 5) + sediment += ( + f" {prev:>10} {layer.d50:>10} " + + f"{layer.sigma:>10} " + + f"{layer.critical_constraint:>10}" + ) + + # Point line + wfile.write(f"{x} {y} {z} {n} {sediment}\n") + @timer def _export_BC(self, t, bounds, repertory, qlog, name="0"): files = [] @@ -210,7 +251,7 @@ class Mage(AbstractSolver): v1 = d[1] if t in ["HYD", "QSO", "LIM"]: - v0 /= 60 # Convert first column to minute + v0 /= 60 # Convert first column to minute f.write(f"{v0:10}{v1:10}\n") @@ -228,6 +269,9 @@ class Mage(AbstractSolver): for tab in ["liquid", "solid", "suspenssion"]: for bound in lst.get_tab(tab): + if bound.node is None: + continue + if bound.bctype == "ZD": AVA.append(bound) elif bound.bctype == "TD" or bound.bctype == "PC": @@ -251,7 +295,10 @@ class Mage(AbstractSolver): # if qlog is not None: # qlog.put(f"Export LAT file") - # with mage_file_open(os.path.join(repertory, f"{name}.LAT"), "w+") as f: + # with mage_file_open( + # os.path.join(repertory, f"{name}.LAT"), + # "w+" + # ) as f: # files.append(f"{name}.LAT") # name = f"{lateral.node.id:3}".replace(" ", "x") @@ -328,13 +375,7 @@ class Mage(AbstractSolver): with mage_file_open(os.path.join(repertory, f"{name}.INI"), "w+") as f: has_ini = False id = 1 - reachs = study.river.edges() - reachs = list( - filter( - lambda e: e.is_enable(), - reachs - ) - ) + reachs = study.river.enable_edges() # TODO put real date... f.write(f"$ date en minutes : 0.00\n") @@ -351,13 +392,13 @@ class Mage(AbstractSolver): id_sec = 1 for d in data: - IR = f"{id:>3}" - IS = f"{id_sec:>3}" + IR = f"{id}" + IS = f"{id_sec}" discharge = f"{d['discharge']:>10.5f}" elevation = f"{d['elevation']:>11.6f}" kp = f"{d['kp']:>9.2f}" - f.write(f" {IR} {IS} {discharge}{elevation} {kp}\n") + f.write(f"{IR} {IS} {discharge} {elevation} {kp}\n") id_sec += 1 id += 1 @@ -366,13 +407,179 @@ class Mage(AbstractSolver): files.append(f"{name}.INI") return files + @timer + def _export_CAS(self, study, repertory, qlog, name="0"): + files = [] + + reservoirs = study.river.reservoir.lst + if len(reservoirs) == 0: + return files + + if qlog is not None: + qlog.put("Export CAS file") + + with mage_file_open(os.path.join(repertory, f"{name}.CAS"), "w+") as f: + files.append(f"{name}.CAS") + + for reservoir in reservoirs: + reservoir.sort() + node = reservoir.node + name = f"{node.id:3}".replace(" ", "x") + f.write(f"* {node.name} ({name}) Reservoir\n") + f.write(f"${name}\n") + f.write(f"*{'Elev(m)':>9}|{'Area(ha)':>10}\n") + + for d in reservoir.data: + v0 = d[0] + v1 = d[1] + + f.write(f"{v0:>10.3f}{v1:>10.3f}\n") + + return files + + @timer + def _export_SIN(self, study, repertory, qlog, name="0"): + files = [] + + sin_dict = { + "ND": "*", + "S1": "D", "S2": "T", "S3": "T", + "OR": "O", "OC": "B", "OV": "F", + "V1": "V", "V2": "W", + "BO": "A", + "UD": "X", + "PO": "P", + } + + hydraulic_structures = study.river.hydraulic_structures.lst + if len(hydraulic_structures) == 0: + return files + + if qlog is not None: + qlog.put("Export SIN file") + + with mage_file_open(os.path.join(repertory, f"{name}.SIN"), "w+") as f: + files.append(f"{name}.SIN") + + for hs in hydraulic_structures: + if not hs.input_reach.is_enable(): + continue + + f.write( + '* ouvrage au pk ' + + f"{hs.input_kp:>12.1f}" + ' ' + + hs.name + '\n' + ) + + for bhs in hs.basic_structures: + reach_id = study.river.get_edge_id(hs.input_reach) + 1 + param_str = ' '.join( + [ + f'{p:>10.3f}' + for p in self._export_SIN_parameters(bhs) + ] + ) + + f.write( + f"{sin_dict[bhs._type]} " + + f"{reach_id} {hs.input_kp:>12.3f} {param_str} " + + f"{bhs.name}\n" + ) + + return files + + def _export_SIN_parameters(self, bhs): + res = [9999.999] * 5 + + if len(bhs) == 5: + res = self._export_SIN_parameters_5(bhs) + elif len(bhs) == 4: + res = self._export_SIN_parameters_4(bhs) + elif len(bhs) == 3: + res = self._export_SIN_parameters_3(bhs) + + return res + + def _export_SIN_parameters_5(self, bhs): + # S2, OR, V1, V2, UD + return [ + bhs._data[0].value, + bhs._data[1].value, + bhs._data[2].value, + bhs._data[3].value, + bhs._data[4].value, + ] + + def _export_SIN_parameters_4(self, bhs): + # S3, OC + res = [ + bhs._data[0].value, + bhs._data[1].value, + bhs._data[2].value, + bhs._data[3].value, + 0.0, + ] + + if bhs._type == "T": # S3 + res = [0.0] + res[:-1] + + return res + + def _export_SIN_parameters_3(self, bhs): + # S1, BO + if bhs._type == "S1": + res = [ + bhs._data[0].value, + bhs._data[1].value, + 0.0, + bhs._data[2].value, + 9999.99, + ] + else: + res = [ + bhs._data[0].value, + bhs._data[1].value, + bhs._data[2].value, + 0.0, + 0.0, + ] + + return res + + @timer + def _export_DEV(self, study, repertory, qlog, name="0"): + files = [] + + if qlog is not None: + qlog.put("Export DEV file") + + with mage_file_open( + os.path.join( + repertory, f"{name}.DEV" + ), "w+" + ) as f: + reachs = study.river.enable_edges() + + id = 1 + for reach in reachs: + f.write(f"YD{id:3}\n") + f.write(f"YG{id:3}\n") + id += 1 + files.append(f"{name}.DEV") + + return files + @timer def _export_REP(self, study, repertory, files, qlog, name="0"): if qlog is not None: qlog.put("Export REP file") # Write header - with mage_file_open(os.path.join(repertory, f"{name}.REP"), "w+") as f: + with mage_file_open( + os.path.join( + repertory, f"{name}.REP" + ), "w+" + ) as f: f.write("confirmation=non\n") for file in files: @@ -391,13 +598,16 @@ class Mage(AbstractSolver): if EXT in ["GRA"]: f.write(f"{EXT} {file}\n") - @timer - def export(self, study, repertory, qlog = None): + def export(self, study, repertory, qlog=None): self._study = study name = study.name.replace(" ", "_") - self._export_ST(study, repertory, qlog, name = name) + # Define GRA file name + self._gra_file = f"{name}.GRA" + self._bin_file = f"{name}.BIN" + + self._export_ST(study, repertory, qlog, name=name) return True @@ -405,13 +615,17 @@ class Mage(AbstractSolver): # RESULTS # ########### - def read_bin(self, study, repertory, results, qlog = None, name="0"): + def read_bin(self, study, repertory, results, qlog=None, name="0"): return @timer - def results(self, study, repertory, qlog = None, name="0"): - results = Results(study = study) - + def results(self, study, repertory, qlog=None, name="0"): + results = Results( + study=study, + solver=self, + repertory=repertory, + name=name, + ) self.read_bin(study, repertory, results, qlog, name=name) return results @@ -420,6 +634,7 @@ class Mage(AbstractSolver): # MAGE 7 # ########## + class Mage7(Mage): _type = "mage7" @@ -438,6 +653,7 @@ class Mage7(Mage): # MAGE 8 # ########## + class Mage8(Mage): _type = "mage8" @@ -476,15 +692,15 @@ class Mage8(Mage): ########## def cmd_args(self, study): - l = super(Mage8, self).cmd_args(study) + lst = super(Mage8, self).cmd_args(study) if study.river.has_sediment(): - l.append("-c=3") + lst.append("-c=3") - return l + return lst @timer - def _export_PAR(self, study, repertory, qlog = None, name = "0"): + def _export_PAR(self, study, repertory, qlog=None, name="0"): files = [] if qlog is not None: @@ -503,15 +719,19 @@ class Mage8(Mage): if name in ["command_line_arguments"]: continue - if name is "mage_compute_reach_volume_balance": - value = "O" if value == "y" else "N" + if name == "compute_reach_volume_balance": + value = "O" if value.lower() == "y" else "N" + + if name == "init_internal": + value = ("p" if value.lower() in ["y", "yes", "true"] + else "") f.write(f"{name} {value}\n") return files @timer - def _export_NET(self, study, repertory, qlog = None, name="0"): + def _export_NET(self, study, repertory, qlog=None, name="0"): files = [] if qlog is not None: @@ -579,20 +799,28 @@ class Mage8(Mage): return files - @timer - def export(self, study, repertory, qlog = None, name="0"): + def export(self, study, repertory, qlog=None, name="0"): self._study = study name = study.name.replace(" ", "_") + # Define GRA file name + self._gra_file = f"{name}.GRA" + self._bin_file = f"{name}.BIN" + + # Generate files files = [] files = self._export_ST(study, repertory, qlog, name=name) files = files + self._export_PAR(study, repertory, qlog, name=name) files = files + self._export_NET(study, repertory, qlog, name=name) - files = files + self._export_bound_cond(study, repertory, qlog, name=name) + files = files + \ + self._export_bound_cond(study, repertory, qlog, name=name) files = files + self._export_RUG(study, repertory, qlog, name=name) files = files + self._export_INI(study, repertory, qlog, name=name) + files = files + self._export_SIN(study, repertory, qlog, name=name) + files = files + self._export_CAS(study, repertory, qlog, name=name) + files = files + self._export_DEV(study, repertory, qlog, name=name) self._export_REP(study, repertory, files, qlog, name=name) return True @@ -602,17 +830,22 @@ class Mage8(Mage): ########### @timer - def read_bin(self, study, repertory, results, qlog = None, name="0"): + def read_bin(self, study, repertory, results, qlog=None, name="0"): fname = os.path.join(repertory, f"{name}.BIN") logger.info(f"read_bin: Start reading '{fname}' ...") with mage_file_open(fname, "r") as f: - newline = lambda: np.fromfile(f, dtype=np.int32, count=1) - endline = lambda: np.fromfile(f, dtype=np.int32, count=1) + def newline(): return np.fromfile(f, dtype=np.int32, count=1) + def endline(): return np.fromfile(f, dtype=np.int32, count=1) - read_int = lambda size: np.fromfile(f, dtype=np.int32, count=size) - read_float = lambda size: np.fromfile(f, dtype=np.float32, count=size) - read_float64 = lambda size: np.fromfile(f, dtype=np.float64, count=size) + def read_int(size): return np.fromfile( + f, dtype=np.int32, count=size) + + def read_float(size): return np.fromfile( + f, dtype=np.float32, count=size) + + def read_float64(size): return np.fromfile( + f, dtype=np.float64, count=size) # Meta data (1st line) newline() @@ -686,7 +919,7 @@ class Mage8(Mage): # Data newline() - ip_to_r = lambda i: iprofiles[ + def ip_to_r(i): return iprofiles[ next( filter( lambda k: k[0] <= i <= k[1], @@ -694,14 +927,15 @@ class Mage8(Mage): ) ) ] - ip_to_ri = lambda r, i: i - reach_offset[r] + def ip_to_ri(r, i): return i - reach_offset[r] ts = set() end = False while not end: n = read_int(1)[0] timestamp = read_float64(1)[0] - key = bytearray(np.fromfile(f, dtype=np.byte, count=1)).decode() + key = bytearray(np.fromfile( + f, dtype=np.byte, count=1)).decode() data = read_float(n) logger.debug(f"read_bin: timestamp = {timestamp} sec") @@ -716,6 +950,13 @@ class Mage8(Mage): # Set data for profile RI reach.set(ri, timestamp, key, d) + if key == "Z": + profile = study.river\ + .current_reach()\ + .reach.profile(ri) + ptX, ptY = profile.get_water_limits(d) + reach.set(ri, timestamp, "ptX", ptX) + reach.set(ri, timestamp, "ptY", ptY) endline() end = newline().size <= 0 @@ -725,7 +966,7 @@ class Mage8(Mage): logger.info(f"read_bin: ... end with {len(ts)} timestamp read") @timer - def read_gra(self, study, repertory, results, qlog = None, name="0"): + def read_gra(self, study, repertory, results, qlog=None, name="0"): if not study.river.has_sediment(): return @@ -733,12 +974,17 @@ class Mage8(Mage): logger.info(f"read_gra: Start reading '{fname}' ...") with mage_file_open(fname, "r") as f: - newline = lambda: np.fromfile(f, dtype=np.int32, count=1) - endline = lambda: np.fromfile(f, dtype=np.int32, count=1) + def newline(): return np.fromfile(f, dtype=np.int32, count=1) + def endline(): return np.fromfile(f, dtype=np.int32, count=1) - read_int = lambda size: np.fromfile(f, dtype=np.int32, count=size) - read_float = lambda size: np.fromfile(f, dtype=np.float32, count=size) - read_float64 = lambda size: np.fromfile(f, dtype=np.float64, count=size) + def read_int(size): return np.fromfile( + f, dtype=np.int32, count=size) + + def read_float(size): return np.fromfile( + f, dtype=np.float32, count=size) + + def read_float64(size): return np.fromfile( + f, dtype=np.float64, count=size) # Meta data (1st line) newline() @@ -753,7 +999,7 @@ class Mage8(Mage): logger.debug(f"read_gra: nb_profile = {nb_profile}") logger.debug(f"read_gra: mage_version = {mage_version}") - if mage_version <= 80: + if mage_version < 80: msg = ( "Read GRA files: " + f"Possible incompatible mage version '{mage_version}', " + @@ -810,7 +1056,7 @@ class Mage8(Mage): endline() # Data - ip_to_r = lambda i: iprofiles[ + def ip_to_r(i): return iprofiles[ next( filter( lambda k: k[0] <= i <= k[1], @@ -818,71 +1064,82 @@ class Mage8(Mage): ) ) ] - ip_to_ri = lambda r, i: i - reach_offset[r] + def ip_to_ri(r, i): return i - reach_offset[r] ts = set() - ind = 0 end = False newline() while not end: n = read_int(1)[0] timestamp = read_float64(1)[0] + with_bedload = read_int(1)[0] - logger.debug(f"read_gra: timestamp = {timestamp} sec") - ts.add(timestamp) + logger.debug(f"read_gra: Number of cross section: {n}") + logger.debug(f"read_gra: Timestamp: {timestamp}") + logger.debug(f"read_gra: Type of bedload: {with_bedload}") endline() - for i in range(n): + npts = [1] * n + if with_bedload == 1: newline() - nsl = read_int(1)[0] + npts = read_int(n) endline() + sum_npts = sum(npts) + logger.debug(f"read_gra: Number of points: {sum_npts}") - # Get current profile id + newline() + nsl = read_int(sum_npts) + logger.debug(f"read_gra: Number of sedimentary layers: {nsl}") + endline() + + newline() + data = read_float64(sum(nsl) * 3) + endline() + + ts.add(timestamp) + + i_pts = 0 + i_data = 0 + # Loop on cross section + for i in range(n): + sec_sl = [] reach = ip_to_r(i) - ri = ip_to_ri(reach, i) - - if nsl > 1: - logger.warning( - "read_gra: " + - "Multiple sediment layers for one profile " + - "is not implemented yet..." - ) - - for j in range(nsl): - newline() - nl = read_int(1)[0] - endline() + p_i = ip_to_ri(reach, i) + # Loop on cross section points + for j in range(npts[i]): sl = [] - for k in range(nl): - newline() - data = read_float64(3) - endline() - - h = data[0] - d50 = data[1] - sigma = data[2] + # Loop on sediment layers + for k in range(nsl[i_pts]): + h = data[i_data] + d50 = data[i_data + 1] + sigma = data[i_data + 2] sl.append((h, d50, sigma)) + i_data += 3 - reach.set(ri, timestamp, "sl", sl) + i_pts += 1 + sec_sl.append(sl) - ind += 1 + reach.set(p_i, timestamp, "sl", sec_sl) + + logger.debug( + f"read_gra: data size = {len(data)} ({i_data} readed)" + ) end = newline().size <= 0 - logger.debug(reachs[0].profiles[0]._data) - results.set("timestamps", ts) + results.set("sediment_timestamps", ts) logger.info(f"read_gra: ... end with {len(ts)} timestamp read") @timer - def results(self, study, repertory, qlog = None): + def results(self, study, repertory, qlog=None): self._study = study name = study.name.replace(" ", "_") - results = super(Mage8, self).results(study, repertory, qlog, name = name) - self.read_gra(study, repertory, results, qlog, name = name) + results = super(Mage8, self).results(study, repertory, qlog, name=name) + self.read_gra(study, repertory, results, qlog, name=name) return results diff --git a/src/Solver/Solvers.py b/src/Solver/Solvers.py index 221f1ff4..514e06c6 100644 --- a/src/Solver/Solvers.py +++ b/src/Solver/Solvers.py @@ -24,9 +24,9 @@ from Solver.Mage import Mage7, Mage8 _translate = QCoreApplication.translate solver_long_name = { - "generic": _translate("Solvers", "Generic"), - # "mage7": _translate("Solvers", "Mage version 7"), - "mage8": _translate("Solvers", "Mage version 8"), + "generic": "Generic", + # "mage7": "Mage v7", + "mage8": "Mage v8", } solver_type_list = { diff --git a/src/View/About/Window.py b/src/View/About/Window.py index 7c269aaf..7798729e 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -19,14 +19,18 @@ import os import logging -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtCore import QCoreApplication _translate = QCoreApplication.translate logger = logging.getLogger() -class AboutWindow(ASubWindow): + +class AboutWindow(PamhyrDialog): + _pamhyr_ui = "about" + _pamhyr_name = "About" + def _path_file(self, filename): return os.path.abspath( os.path.join( @@ -35,10 +39,14 @@ class AboutWindow(ASubWindow): ) ) - - def __init__(self, title="About", parent=None): - super(AboutWindow, self).__init__(name=title, ui="about", parent=parent) - self.ui.setWindowTitle(title) + def __init__(self, study=None, config=None, parent=None): + super(AboutWindow, self).__init__( + title=_translate("About", "About"), + study=study, + config=config, + options=[], + parent=parent + ) # Version with open(self._path_file("VERSION"), "r") as f: diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index b27380a5..bb7a339c 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -21,27 +21,32 @@ import logging from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication ) -from View.BoundaryCondition.Edit.translate import * +from View.BoundaryCondition.Edit.translate import BCETranslate _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, - mode = "time", toolbar=None): + +class Plot(PamhyrPlot): + def __init__(self, mode="time", data=None, + trad=None, canvas=None, toolbar=None, + parent=None): super(Plot, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) + self._table_headers = self._trad.get_dict("table_headers") self._mode = mode def custom_ticks(self): @@ -68,8 +73,8 @@ class Plot(APlot): map( lambda v: str( datetime.fromtimestamp(v[0]) - t0 - ).split(",")[0]\ - .replace("days", _translate("BoundaryCondition", "days"))\ + ).split(",")[0] + .replace("days", _translate("BoundaryCondition", "days")) .replace("day", _translate("BoundaryCondition", "day")), fx ) @@ -84,7 +89,6 @@ class Plot(APlot): self.canvas.axes.set_xticks(ticks=xx, labels=xt, rotation=45) - @timer def draw(self): self.canvas.axes.cla() @@ -109,22 +113,22 @@ class Plot(APlot): # Plot label header = self.data.header self.canvas.axes.set_xlabel( - table_headers[header[0]], color='black', fontsize=10 + self._table_headers[header[0]], color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - table_headers[header[1]], color='black', fontsize=10 + self._table_headers[header[1]], color='black', fontsize=10 ) self.canvas.axes.autoscale_view(True, True, True) self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() - #self.toolbar.update() + # self.toolbar.update() self._init = True @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 35360400..6f5c7e1e 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -23,8 +23,6 @@ from datetime import date, time, datetime, timedelta from tools import trace, timer -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow from View.Tools.PamhyrTable import PamhyrTableModel from PyQt5.QtCore import ( @@ -46,14 +44,13 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( from View.BoundaryCondition.Edit.UndoCommand import ( SetDataCommand, AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, - DuplicateCommand, ) -from View.BoundaryCondition.Edit.translate import * _translate = QCoreApplication.translate logger = logging.getLogger() + class TableModel(PamhyrTableModel): def data(self, index, role): if role == Qt.TextAlignmentRole: diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py index b30331a1..8aa07641 100644 --- a/src/View/BoundaryCondition/Edit/UndoCommand.py +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -29,6 +29,7 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition logger = logging.getLogger() + class SetDataCommand(QUndoCommand): def __init__(self, data, index, column, new_value): QUndoCommand.__init__(self) @@ -46,6 +47,7 @@ class SetDataCommand(QUndoCommand): def redo(self): self._data._set_i_c_v(self._index, self._column, self._new) + class SetMetaDataCommand(QUndoCommand): def __init__(self, data, column, new_value): QUndoCommand.__init__(self) @@ -89,6 +91,7 @@ class AddCommand(QUndoCommand): else: self._data.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, data, rows): QUndoCommand.__init__(self) @@ -108,6 +111,7 @@ class DelCommand(QUndoCommand): def redo(self): self._data.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, data, _reverse): QUndoCommand.__init__(self) @@ -170,25 +174,10 @@ class PasteCommand(QUndoCommand): self._bcs.reverse() def undo(self): - self._data.delete(self._bcs) + self._data.delete_i( + range(self._row, self._row + len(self._bcs)) + ) def redo(self): for bc in self._bcs: self._data.insert(self._row, bc) - - -class DuplicateCommand(QUndoCommand): - def __init__(self, data, rows, bc): - QUndoCommand.__init__(self) - - self._data = data - self._rows = rows - self._bc = deepcopy(bc) - self._bc.reverse() - - def undo(self): - self._data.delete(self._bc) - - def redo(self): - for bc in self._bcs: - self._data.insert(self._rows[0], bc) diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 6e0d6131..4bdbb5ae 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -20,8 +20,8 @@ import logging from tools import timer, trace -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.PamhyrWidget import PamhyrWidget from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate from PyQt5.QtGui import ( @@ -41,11 +41,10 @@ from PyQt5.QtWidgets import ( QHeaderView, QDoubleSpinBox, QVBoxLayout, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.BoundaryCondition.translate import long_types -from View.BoundaryCondition.Edit.translate import table_headers, retranslate +from View.BoundaryCondition.Edit.translate import BCETranslate from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand from View.BoundaryCondition.Edit.Table import TableModel from View.BoundaryCondition.Edit.Plot import Plot @@ -54,16 +53,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class WD50Sigma(AWidget): + +class WD50Sigma(PamhyrWidget): + _pamhyr_ui = "d50sigma" + d50Changed = pyqtSignal(float) sigmaChanged = pyqtSignal(float) def __init__(self, parent=None): super(WD50Sigma, self).__init__( - ui="d50sigma", parent=parent ) - self.parent = parent self.spinBox_d50 = self.find(QDoubleSpinBox, "doubleSpinBox_d50") self.spinBox_sigma = self.find(QDoubleSpinBox, "doubleSpinBox_sigma") @@ -95,80 +95,76 @@ class WD50Sigma(AWidget): def valueChangedSigma(self, value): self.sigmaChanged.emit(value) -class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit boundary condition", - data=None, study=None, parent=None): - self._data = data - self._study = study - self._title = title - self.compute_title() +class EditBoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "EditBoundaryConditions" + _pamhyr_name = "Edit Boundary Conditions" + + def __init__(self, data=None, study=None, config=None, parent=None): + self._data = data + trad = BCETranslate() + self._long_types = trad.get_dict("long_types") + + name = self._pamhyr_name + if self._data is not None: + node_name = (self._data.node.name if self._data.node is not None + else _translate("BoundaryCondition", "Not associate")) + name = ( + _translate("Edit boundary condition", self._pamhyr_name) + + f" - {study.name} " + + f" - {self._data.name} ({self._data.id}) " + + f"({self._long_types[self._data.bctype]} - {node_name})" + ) super(EditBoundaryConditionWindow, self).__init__( - name=self._title, ui="EditBoundaryConditions", parent=parent + title=name, + study=study, + config=config, + trad=trad, + parent=parent ) - self.ui.setWindowTitle(self._title) + self._hash_data.append(data) - self.setup_sc() self.setup_table() self.setup_plot() self.setup_data() self.setup_connections() - def compute_title(self): - if self._data is not None: - node_name = (self._data.node.name if self._data.node is not None - else _translate("BoundaryCondition", "Not associate")) - self._title = ( - _translate("Edit boundary condition", self._title) + - f" - {self._study.name} " + - f" - {self._data.name} ({self._data.id}) " + - f"({long_types[self._data.bctype]} - {node_name})" - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_data(self): self._is_solid = self._data.bctype == "SL" if self._is_solid: layout = self.find(QVBoxLayout, "verticalLayout_table") - self._d50sigma = WD50Sigma(parent = self) + self._d50sigma = WD50Sigma(parent=self) layout.addWidget(self._d50sigma) self._d50sigma.set_d50(self._data.d50) self._d50sigma.set_sigma(self._data.sigma) def setup_table(self): - retranslate() headers = {} + table_headers = self._trad.get_dict("table_headers") for h in self._data.header: headers[h] = table_headers[h] self._delegate_time = PamhyrExTimeDelegate( - data = self._data, - mode = self._study.time_system, - parent = self + data=self._data, + mode=self._study.time_system, + parent=self ) table = self.find(QTableView, "tableView") self._table = TableModel( - table_view = table, - table_headers = headers, - editable_headers = self._data.header, - delegates = { - "time": self._delegate_time, + table_view=table, + table_headers=headers, + editable_headers=self._data.header, + delegates={ + # "time": self._delegate_time, }, - data = self._data, - undo = self._undo_stack, - opt_data = self._study.time_system + data=self._data, + undo=self._undo_stack, + opt_data=self._study.time_system ) table.setModel(self._table) @@ -186,24 +182,19 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.verticalLayout.addWidget(self.canvas) self.plot = Plot( - canvas = self.canvas, - data = self._data, - mode = self._study.time_system, - toolbar = self.toolbar, + canvas=self.canvas, + data=self._data, + mode=self._study.time_system, + trad=self._trad, + toolbar=self.toolbar, ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self.update) if self._is_solid: @@ -252,7 +243,6 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) ) - def add(self): rows = self.index_selected_rows() if len(self._data) == 0 or len(rows) == 0: @@ -284,8 +274,7 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) self.plot.update() - - def copy(self): + def _copy(self): rows = self.index_selected_rows() table = [] @@ -297,9 +286,11 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() + logger.debug(f"paste: h:{header}, d:{data}") + if len(data) == 0: return @@ -311,12 +302,12 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self._table.paste(row, header, data) self.plot.update() - def undo(self): + def _undo(self): self._table.undo() self.plot.update() self.widget_update() - def redo(self): + def _redo(self): self._table.redo() self.plot.update() self.widget_update() diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py index 28b6ec98..9f54ce2a 100644 --- a/src/View/BoundaryCondition/Edit/translate.py +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -18,23 +18,23 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + +from View.BoundaryCondition.translate import BCTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("BoundaryCondition", "X"), - "y": _translate("BoundaryCondition", "Y"), - "time": _translate("BoundaryCondition", "Time"), - "date": _translate("BoundaryCondition", "Date"), - "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), - "z": _translate("BoundaryCondition", "Z (m)"), - "solid": _translate("BoundaryCondition", "Solid (kg/s)"), -} -def retranslate(): - table_headers["x"] = _translate("BoundaryCondition", "X") - table_headers["y"] = _translate("BoundaryCondition", "Y") - table_headers["time"] = _translate("BoundaryCondition", "Time") - table_headers["date"] = _translate("BoundaryCondition", "Date") - table_headers["discharge"] = _translate("BoundaryCondition", "Discharge (m³/s)") - table_headers["z"] = _translate("BoundaryCondition", "Z (m)") - table_headers["solid"] = _translate("BoundaryCondition", "Solid (kg/s)") +class BCETranslate(BCTranslate): + def __init__(self): + super(BCETranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "x": _translate("BoundaryCondition", "X"), + "y": _translate("BoundaryCondition", "Y"), + "time": _translate("BoundaryCondition", "Time"), + "date": _translate("BoundaryCondition", "Date"), + "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), + "z": _translate("BoundaryCondition", "Z (m)"), + "solid": _translate("BoundaryCondition", "Solid (kg/s)"), + } diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py index 377d9aab..aad78fc3 100644 --- a/src/View/BoundaryCondition/Table.py +++ b/src/View/BoundaryCondition/Table.py @@ -44,21 +44,28 @@ from View.Tools.PamhyrTable import PamhyrTableModel from View.BoundaryCondition.UndoCommand import ( SetNameCommand, SetNodeCommand, SetTypeCommand, AddCommand, DelCommand, SortCommand, - MoveCommand, PasteCommand, DuplicateCommand, + MoveCommand, PasteCommand, ) -from View.BoundaryCondition.translate import * +from View.BoundaryCondition.translate import BC_types logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", tab="", parent=None): + def __init__(self, data=None, mode="type", tab="", + trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data self._mode = mode self._tab = tab + self._trad = trad + + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") def createEditor(self, parent, option, index): self.editor = QComboBox(parent) @@ -66,7 +73,7 @@ class ComboBoxDelegate(QItemDelegate): if self._mode == "type": lst = list( map( - lambda k: long_types[k], + lambda k: self._long_types[k], filter( lambda k: self._tab in BC_types[k].compatibility(), BC_types.keys() @@ -97,8 +104,9 @@ class ComboBoxDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() @@ -107,6 +115,14 @@ class ComboBoxDelegate(QItemDelegate): class TableModel(PamhyrTableModel): + def __init__(self, trad=None, **kwargs): + self._trad = trad + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + super(TableModel, self).__init__(trad=trad, **kwargs) + def _setup_lst(self): self._lst = self._data.boundary_condition self._tab = self._opt_data @@ -125,7 +141,7 @@ class TableModel(PamhyrTableModel): return self._lst.get(self._tab, row).name elif self._headers[column] == "type": t = self._lst.get(self._tab, row).bctype - return long_types[t] + return self._long_types[t] elif self._headers[column] == "node": n = self._lst.get(self._tab, row).node if n is None: @@ -145,20 +161,21 @@ class TableModel(PamhyrTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._lst, self._tab,row, value + self._lst, self._tab, row, value ) ) elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() + if v == value) self._undo.push( SetTypeCommand( - self._lst, self._tab,row, BC_types[key] + self._lst, self._tab, row, BC_types[key] ) ) elif self._headers[column] == "node": self._undo.push( SetNodeCommand( - self._lst, self._tab,row, self._data.node(value) + self._lst, self._tab, row, self._data.node(value) ) ) except Exception as e: @@ -173,7 +190,7 @@ class TableModel(PamhyrTableModel): self._undo.push( AddCommand( - self._lst, self._tab,row + self._lst, self._tab, row ) ) diff --git a/src/View/BoundaryCondition/UndoCommand.py b/src/View/BoundaryCondition/UndoCommand.py index bbf8cb2c..fd2923d3 100644 --- a/src/View/BoundaryCondition/UndoCommand.py +++ b/src/View/BoundaryCondition/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList + class SetNameCommand(QUndoCommand): def __init__(self, bcs, tab, index, new_value): QUndoCommand.__init__(self) @@ -42,6 +43,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._bcs.get(self._tab, self._index).name = self._new + class SetNodeCommand(QUndoCommand): def __init__(self, bcs, tab, index, node): QUndoCommand.__init__(self) @@ -51,12 +53,20 @@ class SetNodeCommand(QUndoCommand): self._index = index self._old = self._bcs.get(self._tab, self._index).node self._new = node + self._prev_assoc_to_node = self._bcs.get_assoc_to_node(tab, node) + + def _previous_assoc_node(self, node): + if self._prev_assoc_to_node is not None: + self._prev_assoc_to_node.node = node def undo(self): self._bcs.get(self._tab, self._index).node = self._old + self._previous_assoc_node(self._new) def redo(self): self._bcs.get(self._tab, self._index).node = self._new + self._previous_assoc_node(None) + class SetTypeCommand(QUndoCommand): def __init__(self, bcs, tab, index, _type): @@ -76,6 +86,7 @@ class SetTypeCommand(QUndoCommand): def redo(self): self._bcs.set(self._tab, self._index, self._new) + class AddCommand(QUndoCommand): def __init__(self, bcs, tab, index): QUndoCommand.__init__(self) @@ -94,6 +105,7 @@ class AddCommand(QUndoCommand): else: self._bcs.insert(self._tab, self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, bcs, tab, rows): QUndoCommand.__init__(self) @@ -114,6 +126,7 @@ class DelCommand(QUndoCommand): def redo(self): self._bcs.delete_i(self._tab, self._rows) + class SortCommand(QUndoCommand): def __init__(self, bcs, tab, _reverse): QUndoCommand.__init__(self) @@ -181,26 +194,11 @@ class PasteCommand(QUndoCommand): self._bc.reverse() def undo(self): - self._bcs.delete(self._tab, self._bc) + self._bcs.delete_i( + self._tab, + range(self._row, self._row + len(self._bc)) + ) def redo(self): for bc in self._bc: self._bcs.insert(self._tab, self._row, bc) - - -class DuplicateCommand(QUndoCommand): - def __init__(self, bcs, tab, rows, bc): - QUndoCommand.__init__(self) - - self._bcs = bcs - self._tab = tab - self._rows = rows - self._bc = deepcopy(bc) - self._bc.reverse() - - def undo(self): - self._bcs.delete(self._tab, self._bc) - - def redo(self): - for bc in self._bcs: - self._bcs.insert(self._tab, self._rows[0], bc) diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 7f8c13e2..a007b7a0 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -48,7 +47,7 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( from View.BoundaryCondition.UndoCommand import ( SetNameCommand, SetNodeCommand, SetTypeCommand, AddCommand, DelCommand, SortCommand, - MoveCommand, PasteCommand, DuplicateCommand, + MoveCommand, PasteCommand, ) from View.BoundaryCondition.Table import ( @@ -56,69 +55,69 @@ from View.BoundaryCondition.Table import ( ) from View.Network.GraphWidget import GraphWidget -from View.BoundaryCondition.translate import table_headers, retranslate +from View.BoundaryCondition.translate import BCTranslate from View.BoundaryCondition.Edit.Window import EditBoundaryConditionWindow _translate = QCoreApplication.translate logger = logging.getLogger() -class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Boundary conditions", study=None, parent=None): - self._title = title + " - " + study.name + +class BoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "BoundaryConditions" + _pamhyr_name = "Boundary conditions" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(BoundaryConditionWindow, self).__init__( - name=title, ui="BoundaryConditions", parent=parent + title=name, + study=study, + config=config, + trad=BCTranslate(), + parent=parent ) - self._study = study self._bcs = self._study.river.boundary_condition - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() self._table = {} for t in ["liquid", "solid", "suspenssion"]: self._delegate_type = ComboBoxDelegate( - data = self._study.river, - mode = "type", - tab = t, + trad=self._trad, + data=self._study.river, + mode="type", + tab=t, parent=self ) self._delegate_node = ComboBoxDelegate( - data = self._study.river, - mode = "node", - tab = t, + trad=self._trad, + data=self._study.river, + mode="node", + tab=t, parent=self ) table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - table_view = table, - table_headers = table_headers, - editable_headers = ["name", "type", "node"], - delegates = { + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "type", "node"], + delegates={ "type": self._delegate_type, "node": self._delegate_node, }, - data = self._study.river, - undo = self._undo_stack, - opt_data = t, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + opt_data=t, ) table.setModel(self._table[t]) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -128,7 +127,7 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): def setup_graph(self): self.graph_widget = GraphWidget( self._study.river, - min_size=None, size=(200,200), + min_size=None, size=(200, 200), only_display=True, parent=self ) @@ -141,11 +140,6 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_edit").triggered.connect(self.edit) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def current_tab(self): return self.find(QTabWidget, "tabWidget")\ .currentWidget()\ @@ -202,17 +196,17 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): row = self.index_selected_row() self._table[tab].move_down(row) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() @@ -220,17 +214,17 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): tab = self.current_tab() rows = self.index_selected_rows() for row in rows: - win = self.sub_win_filter_first( - "Edit boundary condition", - contain = [f"({self._bcs.get(tab, row).id})"] - ) + data = self._bcs.get(tab, row) - if win is None: - win = EditBoundaryConditionWindow( - data=self._bcs.get(tab, row), - study=self._study, - parent=self - ) - win.show() - else: - win.activateWindow() + if self.sub_window_exists( + EditBoundaryConditionWindow, + data=[self._study, None, data] + ): + continue + + win = EditBoundaryConditionWindow( + data=data, + study=self._study, + parent=self + ) + win.show() diff --git a/src/View/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py index 78aaec07..97eab96a 100644 --- a/src/View/BoundaryCondition/translate.py +++ b/src/View/BoundaryCondition/translate.py @@ -18,6 +18,8 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, TimeOverZ, TimeOverDischarge, ZOverDischarge, @@ -26,21 +28,6 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( _translate = QCoreApplication.translate -long_types = { - "ND": _translate("BoundaryCondition", "Not defined"), - "PC": _translate("BoundaryCondition", "Ponctual contribution"), - "TZ": _translate("BoundaryCondition", "TimeZ"), - "TD": _translate("BoundaryCondition", "TimeDischarge"), - "ZD": _translate("BoundaryCondition", "ZDischarge"), - "SL": _translate("BoundaryCondition", "Solid"), -} - -table_headers = { - "name": _translate("BoundaryCondition", "Name"), - "type": _translate("BoundaryCondition", "Type"), - "node": _translate("BoundaryCondition", "Node") -} - BC_types = { "ND": NotDefined, "PC": PonctualContribution, @@ -50,7 +37,22 @@ BC_types = { "SL": Solid, } -def retranslate(): - table_headers["name"] = _translate("BoundaryCondition", "Name") - table_headers["type"] = _translate("BoundaryCondition", "Type") - table_headers["node"] = _translate("BoundaryCondition", "Node") + +class BCTranslate(PamhyrTranslate): + def __init__(self): + super(BCTranslate, self).__init__() + + self._sub_dict["long_types"] = { + "ND": _translate("BoundaryCondition", "Not defined"), + "PC": _translate("BoundaryCondition", "Ponctual contribution"), + "TZ": _translate("BoundaryCondition", "Z(t)"), + "TD": _translate("BoundaryCondition", "Q(t)"), + "ZD": _translate("BoundaryCondition", "Q(Z)"), + "SL": _translate("BoundaryCondition", "Solid"), + } + + self._sub_dict["table_headers"] = { + "name": _translate("BoundaryCondition", "Name"), + "type": _translate("BoundaryCondition", "Type"), + "node": _translate("BoundaryCondition", "Node") + } diff --git a/src/View/CheckList/Table.py b/src/View/CheckList/Table.py index 5810c4be..b355eb6c 100644 --- a/src/View/CheckList/Table.py +++ b/src/View/CheckList/Table.py @@ -35,25 +35,12 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, data=None): - super(QAbstractTableModel, self).__init__() - self._headers = ["name", "status"] - self._data = data - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - return options - - def rowCount(self, parent): - return len(self._data) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): row = index.row() column = index.column() @@ -81,12 +68,3 @@ class TableModel(QAbstractTableModel): return self._data[row].summary return QVariant() - - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return self._headers[section] - - return QVariant() - - def update(self): - self.layoutChanged.emit() diff --git a/src/View/CheckList/Translate.py b/src/View/CheckList/Translate.py new file mode 100644 index 00000000..d046365d --- /dev/null +++ b/src/View/CheckList/Translate.py @@ -0,0 +1,33 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class CheckListTranslate(PamhyrTranslate): + def __init__(self): + super(CheckListTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("CheckList", "Name"), + "status": _translate("CheckList", "Status"), + } diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index 4c40ad1f..d96994a8 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -18,8 +18,7 @@ from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -41,33 +40,40 @@ from PyQt5.QtWidgets import ( from View.CheckList.Table import TableModel from View.CheckList.Worker import Worker +from View.CheckList.Translate import CheckListTranslate _translate = QCoreApplication.translate -class CheckListWindow(ASubMainWindow, ListedSubWindow): + +class CheckListWindow(PamhyrWindow): + _pamhyr_ui = "CheckList" + _pamhyr_name = "Check list" signalStatus = pyqtSignal(str) - def __init__(self, title="Check list", - autorun:bool=True, + def __init__(self, autorun: bool = True, study=None, config=None, solver=None, parent=None): - self._title = title + " - " + study.name - self._autorun = autorun - - self._study = study - self._config = config self._solver = solver - self._parent = parent + + name = self._pamhyr_name + " - " + study.name super(CheckListWindow, self).__init__( - name=self._title, ui="CheckList", parent=parent + title=name, + study=study, + config=config, + trad=CheckListTranslate(), + options=[], + parent=parent ) - self.ui.setWindowTitle(self._title) + + # Add solver to hash computation data + self._hash_data.append(self._solver) self._checker_list = ( - self._study.checkers() + self._solver.checkers() + self._study.checkers() + + self._solver.checkers() ) self.setup_table() @@ -78,16 +84,12 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): def setup_table(self): table = self.find(QTableView, f"tableView") - self._table = TableModel( - data = self._checker_list, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + data=self._checker_list, ) - table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) - def setup_progress_bar(self): self._progress = self.find(QProgressBar, f"progressBar") self._p = 0 # Progress current step @@ -98,7 +100,8 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): def setup_connections(self): self.find(QPushButton, "pushButton_ok").clicked.connect(self.accept) self.find(QPushButton, "pushButton_retry").clicked.connect(self.retry) - self.find(QPushButton, "pushButton_cancel").clicked.connect(self.reject) + self.find(QPushButton, "pushButton_cancel")\ + .clicked.connect(self.reject) def setup_thread(self): self._worker = Worker(self._study, self._checker_list) @@ -122,7 +125,8 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): def _compute_status(self): ok = len(list(filter(lambda c: c.is_ok(), self._checker_list))) - warning = len(list(filter(lambda c: c.is_warning(), self._checker_list))) + warning = len( + list(filter(lambda c: c.is_warning(), self._checker_list))) error = len(list(filter(lambda c: c.is_error(), self._checker_list))) return ok, warning, error @@ -166,7 +170,7 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): self.update_statusbar() - def update(self, key:str): + def update(self, key: str): if key == "start": self.start_compute() self.info_compute("Starting ...") @@ -189,4 +193,4 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): def accept(self): self._parent.solver_log(self._solver) - #self.end() + # self.end() diff --git a/src/View/CheckList/Worker.py b/src/View/CheckList/Worker.py index 66e34d2c..834d8975 100644 --- a/src/View/CheckList/Worker.py +++ b/src/View/CheckList/Worker.py @@ -22,6 +22,7 @@ from PyQt5.QtCore import ( Qt, QObject, pyqtSlot, pyqtSignal ) + class Worker(QObject): signalStatus = pyqtSignal(str) @@ -42,6 +43,6 @@ class Worker(QObject): self.signalStatus.emit(checker.name) # Run checker - checker.run(self._study) + checker._run(self._study) self.signalStatus.emit("progress") diff --git a/src/View/Configure/Solver/Window.py b/src/View/Configure/Solver/Window.py index c295481f..893def20 100644 --- a/src/View/Configure/Solver/Window.py +++ b/src/View/Configure/Solver/Window.py @@ -16,7 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from Solver.Solvers import solver_type_list from Solver.GenericSolver import GenericSolver @@ -24,12 +24,23 @@ from PyQt5.QtWidgets import ( QPushButton, ) -class ConfigureSolverWindow(ASubWindow): - def __init__(self, data=None, title="Configuration : Add Solver", parent=None): + +class ConfigureSolverWindow(PamhyrDialog): + _pamhyr_ui = "ConfigureAddSolverDialog" + _pamhyr_name = "Add/Edit Solver" + + def __init__(self, data=None, config=None, parent=None): + if data is not None: + name = f"Edit Solver - {data.name}" + else: + name = "Add a new Solver" + super(ConfigureSolverWindow, self).__init__( - name=title, ui="ConfigureAddSolverDialog", parent=parent + title=name, + config=config, + options=[], + parent=parent ) - self.ui.setWindowTitle(title) # Combo box item for solver in solver_type_list: @@ -37,10 +48,10 @@ class ConfigureSolverWindow(ASubWindow): # Data to return self.data = data - if not self.data is None: + if self.data is not None: self.copy_data() - self.connect() + self.setup_connection() def copy_data(self): self.set_combobox_text("comboBox_solver", self.data.type) @@ -53,20 +64,23 @@ class ConfigureSolverWindow(ASubWindow): self.set_line_edit_text("lineEdit_output", self.data._path_output) self.set_line_edit_text("lineEdit_output_cmd", self.data._cmd_output) - def connect(self): + def setup_connection(self): # File button buttons = { "pushButton_input": (lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text("lineEdit_input", f[0]) + select_file=True, + callback=lambda f: self.set_line_edit_text( + "lineEdit_input", f[0]) )), "pushButton_solver": (lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text("lineEdit_solver", f[0]) + select_file=True, + callback=lambda f: self.set_line_edit_text( + "lineEdit_solver", f[0]) )), "pushButton_output": (lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text("lineEdit_output", f[0]) + select_file=True, + callback=lambda f: self.set_line_edit_text( + "lineEdit_output", f[0]) )), } @@ -82,9 +96,11 @@ class ConfigureSolverWindow(ASubWindow): else: # Build new solver from selected type stype = self.get_combobox_text("comboBox_solver") - self.data = solver_type_list[stype](self.get_line_edit_text("lineEdit_name")) + self.data = solver_type_list[stype]( + self.get_line_edit_text("lineEdit_name")) - self.data.description = self.get_line_edit_text("lineEdit_description") + self.data.description = self.get_line_edit_text( + "lineEdit_description") self.data.set_input( self.get_line_edit_text("lineEdit_input"), self.get_line_edit_text("lineEdit_input_cmd") diff --git a/src/View/Configure/Translate.py b/src/View/Configure/Translate.py new file mode 100644 index 00000000..5355d8a5 --- /dev/null +++ b/src/View/Configure/Translate.py @@ -0,0 +1,34 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class ConfigureTranslate(PamhyrTranslate): + def __init__(self): + super(ConfigureTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("Configure", "Name"), + "type": _translate("Configure", "Type"), + "description": _translate("Configure", "Description"), + } diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 23a371ac..f380e868 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -21,13 +21,14 @@ import logging from copy import deepcopy from config import Config -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog +from View.Tools.PamhyrTable import PamhyrTableModel from View.Stricklers.Table import TableModel -from View.Stricklers.translate import * +from View.Stricklers.translate import StricklersTranslate from View.Stricklers.UndoCommand import * +from View.Configure.Translate import ConfigureTranslate from View.Configure.Solver.Window import ConfigureSolverWindow from PyQt5.QtGui import ( @@ -48,126 +49,109 @@ from Solver.Solvers import solver_long_name logger = logging.getLogger() -class SolverTableModel(QAbstractTableModel): - def __init__(self, headers=[], rows=[]): - super(QAbstractTableModel, self).__init__() - self.rows = rows - self.headers = headers - - def rowCount(self, parent): - return len(self.rows) - - def columnCount(self, parent): - return len(self.headers) +class SolverTableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() - ret = self.rows[index.row()][self.headers[index.column()]] + ret = self._data[index.row()][self._headers[index.column()]] - if self.headers[index.column()] == "type": + if self._headers[index.column()] == "type": ret = solver_long_name[ret] return ret - def headerData(self, section, orientation, role): - if (role == Qt.ItemDataRole.DisplayRole and - orientation == Qt.Orientation.Horizontal): - return self.headers[section].capitalize() - - if (role == Qt.ItemDataRole.DisplayRole and - orientation == Qt.Orientation.Vertical): - return section + 1 # Start at 1 - - return QVariant() - def removeRow(self, index): - del self.rows[index.row()] + del self._data[index.row()] self.layoutChanged.emit() def add_solver(self, solver): - self.rows.append(solver) + self._data.append(solver) self.layoutChanged.emit() def change_solver(self, solver, index): - self.rows[index.row()] = solver + self._data[index.row()] = solver self.layoutChanged.emit() -class ConfigureWindow(ASubWindow, ListedSubWindow): - def __init__(self, conf=None, title="Configure", parent=None): +class ConfigureWindow(PamhyrDialog): + _pamhyr_ui = "ConfigureDialog" + _pamhyr_name = "Configure" + + def __init__(self, config=None, parent=None): + if config is None: + config = Config() + super(ConfigureWindow, self).__init__( - name=title, ui="ConfigureDialog", parent=parent + title=self._pamhyr_name, + config=config, + trad=ConfigureTranslate(), + options=[], + parent=parent ) - self.ui.setWindowTitle(title) - if conf is None: - self.conf = Config() - else: - self.conf = conf + self._trad_stricklers = StricklersTranslate() - self.setup_sc() + self.setup_custom_sc() + self.setup_solver() + self.setup_stricklers() + self.setup_data() + self.setup_connection() - # Solver + def setup_solver(self): table = self.find(QTableView, "tableView_solver") self.solver_table_model = SolverTableModel( - headers = ["name", "type", "description"], - rows = conf.solvers + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + data=self._config.solvers ) - table.setModel(self.solver_table_model) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) + def setup_stricklers(self): + table = self.find(QTableView, f"tableView_stricklers") + self._stricklers = deepcopy(self._config.stricklers) + self._stricklers_table = TableModel( + table_view=table, + table_headers=self._trad_stricklers.get_dict("table_headers"), + editable_headers=["name", "comment", "minor", "medium"], + data=self._stricklers, + undo=self._undo_stack, + ) + + def setup_data(self): # Meshing_Tool - self.set_line_edit_text("lineEdit_meshing_tool", self.conf.meshing_tool) + self.set_line_edit_text("lineEdit_meshing_tool", + self._config.meshing_tool) # Const - self.set_line_edit_text("lineEdit_segment", str(self.conf.segment)) - self.set_line_edit_text("lineEdit_max_listing", str(self.conf.max_listing)) + self.set_line_edit_text("lineEdit_segment", str(self._config.segment)) + self.set_line_edit_text("lineEdit_max_listing", + str(self._config.max_listing)) # Backup - self.set_check_box("checkBox_backup", self.conf.backup_enable) - self.set_line_edit_text("lineEdit_backup_path", self.conf.backup_path) - self.set_time_edit("timeEdit_backup_frequence", self.conf.backup_frequence) - self.set_spin_box("spinBox_backup_max", self.conf.backup_max) - - self.find(QTableView, "tableView_solver").resizeColumnsToContents() - self.connect() - - # Stricklers - table = self.find(QTableView, f"tableView_stricklers") - self._stricklers = deepcopy(self.conf.stricklers) - self._stricklers_table = TableModel( - data = self._stricklers, - undo = self._undo_stack, - ) - table.setModel(self._stricklers_table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) + self.set_check_box("checkBox_backup", self._config.backup_enable) + self.set_line_edit_text("lineEdit_backup_path", + self._config.backup_path) + self.set_time_edit("timeEdit_backup_frequence", + self._config.backup_frequence) + self.set_spin_box("spinBox_backup_max", self._config.backup_max) # Editor - self.set_line_edit_text("lineEdit_editor_cmd", str(self.conf.editor)) + self.set_line_edit_text("lineEdit_editor_cmd", + str(self._config.editor)) # Language languages = Config.languages() for lang in languages: self.combobox_add_item("comboBox_language", lang) - if self.conf.lang == languages[lang]: + if self._config.lang == languages[lang]: self.set_combobox_text("comboBox_language", lang) - def setup_sc(self): - self._undo_stack = QUndoStack() + def setup_custom_sc(self): + self._debug_sc = QShortcut(QKeySequence("Ctrl+G"), self) + self._debug_sc.activated.connect(self.set_debug) - self.debug_sc = QShortcut(QKeySequence("Ctrl+G"), self) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def connect(self): + def setup_connection(self): buttons = { # Solvers "pushButton_solver_add": self.add_solver, @@ -179,14 +163,14 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): "pushButton_stricklers_sort": self.sort_stricklers, # Others "pushButton_backup_path": lambda: self.file_dialog( - select_file = False, - callback = lambda f: self.set_line_edit_text( + select_file=False, + callback=lambda f: self.set_line_edit_text( "lineEdit_backup_path", f[0] ) ), - "pushButton_meshing_tool" : lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text( + "pushButton_meshing_tool": lambda: self.file_dialog( + select_file=True, + callback=lambda f: self.set_line_edit_text( "lineEdit_meshing_tool", f[0] ) ), @@ -195,33 +179,36 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): for button in buttons: self.find(QPushButton, button).clicked.connect(buttons[button]) - self.debug_sc.activated.connect(self.set_debug) - def accept(self): # Solvers - self.conf.solvers = self.solver_table_model.rows.copy() + self._config.solvers = self.solver_table_model._data.copy() # Meshing_Tool - self.conf.meshing_tool = self.get_line_edit_text("lineEdit_meshing_tool") + self._config.meshing_tool = self.get_line_edit_text( + "lineEdit_meshing_tool") # Const - self.conf.segment = self.get_line_edit_text("lineEdit_segment") - self.conf.max_listing = self.get_line_edit_text("lineEdit_max_listing") + self._config.segment = self.get_line_edit_text("lineEdit_segment") + self._config.max_listing = self.get_line_edit_text( + "lineEdit_max_listing") # Backup - self.conf.backup_enable = self.get_check_box("checkBox_backup") - self.conf.backup_path = self.get_line_edit_text("lineEdit_backup_path") - self.conf.backup_frequence = self.get_time_edit("timeEdit_backup_frequence") - self.conf.backup_max = self.get_spin_box("spinBox_backup_max") + self._config.backup_enable = self.get_check_box("checkBox_backup") + self._config.backup_path = self.get_line_edit_text( + "lineEdit_backup_path") + self._config.backup_frequence = self.get_time_edit( + "timeEdit_backup_frequence") + self._config.backup_max = self.get_spin_box("spinBox_backup_max") # Stricklers - self.conf.stricklers = deepcopy(self._stricklers) + self._config.stricklers = deepcopy(self._stricklers) # Editor - self.conf.editor = self.get_line_edit_text("lineEdit_editor_cmd") + self._config.editor = self.get_line_edit_text("lineEdit_editor_cmd") # Language - self.conf.lang = Config.languages()[self.get_combobox_text("comboBox_language")] + self._config.lang = Config.languages( + )[self.get_combobox_text("comboBox_language")] self.end() @@ -230,26 +217,30 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): self.end() def end(self): - self.conf.save() + self._config.save() self.close() # Debug + def set_debug(self): - self.conf.debug = not self.conf.debug - logger.info(f"Debug mode set : {self.conf.debug}") + self._config.debug = not self._config.debug + logger.info(f"Debug mode set : {self._config.debug}") self.parent.setup_debug_mode() # Solvers def edit_solver(self): - indexes = self.find(QTableView, "tableView_solver").selectionModel().selectedRows() + indexes = self.find( + QTableView, "tableView_solver").selectionModel().selectedRows() for index in indexes: self.edit_solver = ConfigureSolverWindow( - data=self.solver_table_model.rows[index.row()], + data=self.solver_table_model._data[index.row()], + config=self._config, parent=self ) if self.edit_solver.exec_(): - self.solver_table_model.change_solver(self.edit_solver.data, index) + self.solver_table_model.change_solver( + self.edit_solver.data, index) def add_solver(self): dialog_solver = ConfigureSolverWindow(parent=self) @@ -257,7 +248,8 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): self.solver_table_model.add_solver(dialog_solver.data) def remove_solver(self): - indices = self.find(QTableView, "tableView_solver").selectionModel().selectedRows() + indices = self.find( + QTableView, "tableView_solver").selectionModel().selectedRows() for index in sorted(indices): self.solver_table_model.removeRow(index) diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index 9b14aa20..a7c5b7af 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -20,8 +20,8 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.PamhyrPythonEditor import PamhyrPythonEditor from PyQt5.QtGui import ( QKeySequence, @@ -38,34 +38,42 @@ from PyQt5.QtWidgets import ( QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, QComboBox, QVBoxLayout, QHeaderView, QTabWidget, - QProgressBar, QLabel, QTextEdit, + QProgressBar, QLabel, QTextEdit, QHBoxLayout, ) +from PyQt5.Qsci import QsciScintilla, QsciLexerPython + logger = logging.getLogger() _translate = QCoreApplication.translate -class ReplWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Debug REPL", - study=None, config=None, + +class ReplWindow(PamhyrWindow): + _pamhyr_ui = "DebugRepl" + _pamhyr_name = "Debug REPL" + + def __init__(self, study=None, config=None, solver=None, parent=None): - self._title = title - - self._study = study - self._config = config - self._parent = parent - super(ReplWindow, self).__init__( - name=self._title, ui="DebugRepl", parent=parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) - self.ui.setWindowTitle(self._title) self.__debug_exec_result__ = None self._history = [] self._history_ind = 0 + self.setup_editor() self.setup_connections() + def setup_editor(self): + layout = self.find(QHBoxLayout, "horizontalLayout") + self._editor = PamhyrPythonEditor() + layout.insertWidget(0, self._editor) + def setup_connections(self): self._hup_sc = QShortcut(QKeySequence("ctrl+Up"), self) self._hdown_sc = QShortcut(QKeySequence("ctrl+Down"), self) @@ -77,19 +85,19 @@ class ReplWindow(ASubMainWindow, ListedSubWindow): def history_up(self): if self._history_ind < len(self._history): self._history_ind += 1 - self.set_plaintext_edit_text("plainTextEdit", self._history[-self._history_ind]) + self._editor.setText(self._history[-self._history_ind]) def history_down(self): if self._history_ind > 0: self._history_ind -= 1 - self.set_plaintext_edit_text("plainTextEdit", self._history[-self._history_ind]) + self._editor.setText(self._history[-self._history_ind]) else: - self.set_plaintext_edit_text("plainTextEdit", "") + self._editor.setText("") def eval_python(self): - code = self.get_plaintext_edit_text("plainTextEdit") + code = self._editor.text() self._history.append(code) - self.set_plaintext_edit_text("plainTextEdit", "") + self._editor.setText("") self._history_ind = 0 # Code to rich_code @@ -97,7 +105,7 @@ class ReplWindow(ASubMainWindow, ListedSubWindow): # Add return variable for results display at last row rich_code[-1] = "self.__debug_exec_result__ = " + rich_code[-1] rich_code = "\n".join(rich_code) - logger.debug(f"User debug command : {rich_code}") + logger.debug(f"User debug command : \n{rich_code}") try: value = exec(rich_code) value = self.__debug_exec_result__ diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py new file mode 100644 index 00000000..9f64da52 --- /dev/null +++ b/src/View/Doc/Window.py @@ -0,0 +1,102 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import sys +import logging +import subprocess + +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QAction +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings + +from View.Tools.PamhyrWindow import PamhyrWindow + +logger = logging.getLogger() + + +class DocWindow(PamhyrWindow): + _pamhyr_ui = "WebView" + _pamhyr_name = "Doc" + + @classmethod + def _path_file(cls, filename): + if ".py" in sys.argv[0]: + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "doc", filename + ) + ) + + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "..", "doc", filename + ) + ) + + def __init__(self, filename=None, + study=None, config=None, + parent=None): + super(DocWindow, self).__init__( + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent + ) + + self._path = self._path_file(filename) + + self.setup_web_engine() + self.setup_url(self._path) + self.setup_connection() + + def setup_connection(self): + self.findChild(QAction, "action_back").triggered.connect(self.back) + self.findChild(QAction, "action_forward").triggered.connect( + self.forward) + + self.findChild(QAction, "action_open").triggered.connect(self.open) + + def setup_web_engine(self): + vl = self.find(QVBoxLayout, "verticalLayout") + self._web_view = QWebEngineView() + + settings = self._web_view.settings() + settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) + settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) + settings.setAttribute(QWebEngineSettings.PdfViewerEnabled, True) + + vl.addWidget(self._web_view) + + def setup_url(self, filename): + logger.info(f"Open documentation : {filename}") + self._web_view.setUrl(QUrl(f"file://{self._path_file(filename)}")) + + def back(self): + self._web_view.back() + + def forward(self): + self._web_view.forward() + + def open(self): + url = QUrl(f"file://{self._path}") + QDesktopServices.openUrl(url) diff --git a/src/View/DummyWindow.py b/src/View/DummyWindow.py index 05117bca..aaab178c 100644 --- a/src/View/DummyWindow.py +++ b/src/View/DummyWindow.py @@ -16,9 +16,15 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrWindow -class DummyWindow(ASubWindow): - def __init__(self, title="Dummy", parent=None): - super(DummyWindow, self).__init__(name=title, ui="dummy", parent=parent) - self.ui.setWindowTitle(title) + +class DummyWindow(PamhyrWindow): + __ui = "Dummy" + __name = "Pamhyr Dummy Window" + + def __init__(self, parent=None): + super(DummyWindow, self).__init__( + title=self.__name, + parent=parent, + ) diff --git a/src/View/Frictions/PlotStricklers.py b/src/View/Frictions/PlotStricklers.py index 71ff6e6f..6e5c4e0c 100644 --- a/src/View/Frictions/PlotStricklers.py +++ b/src/View/Frictions/PlotStricklers.py @@ -16,26 +16,29 @@ # -*- coding: utf-8 -*- +import logging + from tools import timer, flatten -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication ) +logger = logging.getLogger() + _translate = QCoreApplication.translate -class PlotStricklers(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotStricklers, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) +class PlotStricklers(PamhyrPlot): def draw_frictions(self, frictions, color="r"): - lst = frictions - lst.sort(key = lambda s: s.begin_kp) + lst = sorted( + filter( + lambda f: f.is_full_defined(), + frictions + ), + key=lambda s: s.begin_kp + ) coef = flatten( map( @@ -64,7 +67,6 @@ class PlotStricklers(APlot): color=color, lw=1. ) - @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -75,16 +77,16 @@ class PlotStricklers(APlot): self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Stricklers"), - color='green', fontsize=11 + color='black', fontsize=11 ) self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "Kp (m)"), - color='green', fontsize=11 + color='black', fontsize=11 ) kp = self.data.reach.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) frictions = self.data.frictions @@ -115,6 +117,6 @@ class PlotStricklers(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/Frictions/Table.py b/src/View/Frictions/Table.py index ef9dc32f..e1678fdc 100644 --- a/src/View/Frictions/Table.py +++ b/src/View/Frictions/Table.py @@ -41,14 +41,18 @@ from View.Frictions.UndoCommand import ( MoveCommand, PasteCommand, DuplicateCommand, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.Frictions.translate import * logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, study=None, mode="stricklers", parent=None): + def __init__(self, data=None, study=None, + mode="stricklers", parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data @@ -84,8 +88,9 @@ class ComboBoxDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() @@ -93,26 +98,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, data=None, study=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._data = data - self._study = study - self._undo = undo - self._frictions = self._data.frictions - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._frictions) - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data.frictions + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -122,30 +111,24 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._frictions.get(row).name + return self._lst.get(row).name elif self._headers[column] == "begin_kp": - return self._frictions.get(row).begin_kp + return self._lst.get(row).begin_kp elif self._headers[column] == "end_kp": - return self._frictions.get(row).end_kp + return self._lst.get(row).end_kp elif self._headers[column] == "begin_strickler": - value = self._frictions.get(row).begin_strickler - if value == None: + value = self._lst.get(row).begin_strickler + if value is None: return _translate("Frictions", "Not defined") return str(value) elif self._headers[column] == "end_strickler": - value = self._frictions.get(row).end_strickler - if value == None: + value = self._lst.get(row).end_strickler + if value is None: return _translate("Frictions", "Not defined") return str(value) return QVariant() - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -157,31 +140,31 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "begin_kp": self._undo.push( SetBeginCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "end_kp": self._undo.push( SetEndCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "begin_strickler": self._undo.push( SetBeginStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + self._lst, row, self._study.river.strickler(value) ) ) elif self._headers[column] == "end_strickler": self._undo.push( SetEndStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + self._lst, row, self._study.river.strickler(value) ) ) except Exception as e: @@ -196,7 +179,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._frictions, row, self._data + self._lst, row, self._data ) ) @@ -208,7 +191,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._frictions, rows + self._lst, rows ) ) @@ -220,7 +203,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SortCommand( - self._frictions, False + self._lst, False ) ) @@ -237,7 +220,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._frictions, "up", row + self._lst, "up", row ) ) @@ -245,7 +228,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._frictions): + if row > len(self._lst): return target = row @@ -254,7 +237,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._frictions, "down", row + self._lst, "down", row ) ) diff --git a/src/View/Frictions/UndoCommand.py b/src/View/Frictions/UndoCommand.py index ef902f07..5aba2a7c 100644 --- a/src/View/Frictions/UndoCommand.py +++ b/src/View/Frictions/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.Friction.Friction import Friction from Model.Friction.FrictionList import FrictionList + class SetNameCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -41,6 +42,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).name = self._new + class SetBeginCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -56,6 +58,7 @@ class SetBeginCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).begin_kp = float(self._new) + class SetEndCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -87,6 +90,7 @@ class SetBeginStricklerCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).begin_strickler = self._new + class SetEndStricklerCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -118,6 +122,7 @@ class SetEdgeCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).edge = self._new + class AddCommand(QUndoCommand): def __init__(self, frictions, index, reach): QUndoCommand.__init__(self) @@ -137,6 +142,7 @@ class AddCommand(QUndoCommand): else: self._frictions.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, frictions, rows): QUndoCommand.__init__(self) @@ -156,6 +162,7 @@ class DelCommand(QUndoCommand): def redo(self): self._frictions.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, frictions, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 060be330..95b41d8b 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -48,77 +47,76 @@ from View.Frictions.Table import ( TableModel, ComboBoxDelegate ) -from View.Plot.MplCanvas import MplCanvas +from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Geometry.PlotKPZ import PlotKPZ from View.Frictions.PlotStricklers import PlotStricklers -from View.Frictions.translate import table_headers, retranslate +from View.Frictions.translate import FrictionsTranslate from View.Stricklers.Window import StricklersWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() -class FrictionsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Frictions", study=None, parent=None): - self._study = study - self._reach = self._study.river.current_reach() + +class FrictionsWindow(PamhyrWindow): + _pamhyr_ui = "Frictions" + _pamhyr_name = "Edit frictions" + + def __init__(self, reach=None, study=None, config=None, parent=None): + if reach is not None: + self._reach = reach + else: + self._reach = study.river.current_reach() + self._frictions = self._reach.frictions - self.setup_title(title) - - super(FrictionsWindow, self).__init__( - name=title, ui="Frictions", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._reach.name ) - self.setup_sc() + super(FrictionsWindow, self).__init__( + title=name, + study=study, + config=config, + trad=FrictionsTranslate(), + parent=parent + ) + + # Add reach to hash computation data + self._hash_data.append(self._reach) + self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() self._table = {} - table = self.find(QTableView, f"tableView") - self._table = TableModel( - data = self._reach, - study = self._study, - undo = self._undo_stack, - ) - table.setModel(self._table) - self._delegate_stricklers = ComboBoxDelegate( - data = self._reach, - study = self._study, - mode = "stricklers", + data=self._reach, + study=self._study, + mode="stricklers", parent=self ) - table.setItemDelegateForColumn( - 3, self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=[ + "name", "begin_kp", "end_kp", + "begin_strickler", "end_strickler" + ], + delegates={ + "begin_strickler": self._delegate_stricklers, + "end_strickler": self._delegate_stricklers, + }, + data=self._reach, + undo=self._undo_stack, + opt_data=self._study ) - table.setItemDelegateForColumn( - 4, self._delegate_stricklers - ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) @@ -130,10 +128,10 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout.addWidget(self.canvas) self.plot = PlotKPZ( - canvas = self.canvas, - data = self._reach.reach, - toolbar = None, - display_current = False + canvas=self.canvas, + data=self._reach.reach, + toolbar=None, + display_current=False ) self.plot.draw() @@ -143,23 +141,18 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout_2.addWidget(self.canvas_2) self.plot_2 = PlotStricklers( - canvas = self.canvas_2, - data = self._reach, - toolbar = None + canvas=self.canvas_2, + data=self._reach, + toolbar=None ) self.plot_2.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.find(QAction, "action_edit_stricklers").triggered.connect(self.edit_stricklers) - - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) + self.find(QAction, "action_edit_stricklers").triggered.connect( + self.edit_stricklers) table = self.find(QTableView, f"tableView") table.selectionModel()\ @@ -195,17 +188,17 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): highlight = (sec.begin_kp, sec.end_kp) self.plot = PlotKPZ( - canvas = self.canvas, - data = reach, - toolbar = None, - display_current = False + canvas=self.canvas, + data=reach, + toolbar=None, + display_current=False ) self.plot.draw(highlight=highlight) self.plot = PlotStricklers( - canvas = self.canvas_2, - data = data, - toolbar = None + canvas=self.canvas_2, + data=data, + toolbar=None ) self.plot.draw(highlight=highlight) @@ -234,30 +227,28 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): row = self.index_selected_row() self._table.move_down(row) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() def edit_stricklers(self): - strick = self.sub_win_filter_first( - "Stricklers", - contain = [] - ) + if self.sub_window_exists( + StricklersWindow, + data=[self._study, self.parent.conf] + ): + return - if strick is None: - strick = StricklersWindow( - study = self._study, - config = self.parent.conf, - parent = self - ) - strick.show() - else: - strick.activateWindow() + strick = StricklersWindow( + study=self._study, + config=self.parent.conf, + parent=self + ) + strick.show() diff --git a/src/View/Frictions/translate.py b/src/View/Frictions/translate.py index 68bebb96..15f8dc7d 100644 --- a/src/View/Frictions/translate.py +++ b/src/View/Frictions/translate.py @@ -18,21 +18,20 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("Frictions", "Name"), - # "edge": _translate("Frictions", "Reach"), - "begin_kp": _translate("Frictions", "Begin kp (m)"), - "end_kp": _translate("Frictions", "End kp (m)"), - "begin_strickler": _translate("Frictions", "Begin strickler"), - "end_strickler": _translate("Frictions", "End strickler"), -} -def retranslate(): - table_headers["name"] = _translate("Frictions", "Name") - # table_headers["edge"] = _translate("Frictions", "Reach") - table_headers["begin_kp"] = _translate("Frictions", "Begin kp (m)") - table_headers["end_kp"] = _translate("Frictions", "End kp (m)") - table_headers["begin_strickler"] = _translate("Frictions", "Begin strickler") - table_headers["end_strickler"] = _translate("Frictions", "End strickler") +class FrictionsTranslate(PamhyrTranslate): + def __init__(self): + super(FrictionsTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + # "name": _translate("Frictions", "Name"), + # "edge": _translate("Frictions", "Reach"), + "begin_kp": _translate("Frictions", "Begin kp (m)"), + "end_kp": _translate("Frictions", "End kp (m)"), + "begin_strickler": _translate("Frictions", "Begin strickler"), + "end_strickler": _translate("Frictions", "End strickler"), + } diff --git a/src/View/Geometry/PlotAC.py b/src/View/Geometry/PlotAC.py index e43f39e9..968f16eb 100644 --- a/src/View/Geometry/PlotAC.py +++ b/src/View/Geometry/PlotAC.py @@ -19,7 +19,7 @@ import logging from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,12 +29,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotAC(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, plot_xy=None): + +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + plot_xy=None, parent=None): super(PlotAC, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self.plot_xy = plot_xy @@ -68,17 +72,19 @@ class PlotAC(APlot): self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "Transverse abscissa (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Height (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.figure.tight_layout() - label_before_plot_selected = _translate("MainWindow_reach", "Previous cross-section") + label_before_plot_selected = _translate( + "MainWindow_reach", "Previous cross-section") label_plot_selected = _translate("MainWindow_reach", "Cross-section") - label_after_plot_selected = _translate("MainWindow_reach", "Next cross-section") + label_after_plot_selected = _translate( + "MainWindow_reach", "Next cross-section") color_before_plot_selected = "k" # 'grey' color_plot_selected = 'b' color_after_plot_selected = 'm' @@ -130,10 +136,12 @@ class PlotAC(APlot): x_gl_complete.append(station[i]) y_gl_complete.append(elevation[i]) - color_scat_complete_gl.append(self.color_complete_gl[lcomplete.index(txt)]) + color_scat_complete_gl.append( + self.color_complete_gl[lcomplete.index(txt)]) elif txt.strip() in self.incomplete_gl: annotate = self.canvas.axes.annotate( - txt, (station[i], elevation[i]), horizontalalignment='left', + txt, (station[i], elevation[i] + ), horizontalalignment='left', verticalalignment='top', annotation_clip=True, fontsize=11, color=self.color_incomplete_gl[ lincomplete.index(txt) @@ -167,18 +175,21 @@ class PlotAC(APlot): self.canvas.axes.cla() self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "Abscisse en travers (m)"), - color='green', fontsize=12 + _translate("MainWindow_reach", "Transverse abscissa (m)"), + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Cote (m)"), - color='green', fontsize=12 + _translate("MainWindow_reach", "Height (m)"), + color='black', fontsize=10 ) self.canvas.figure.tight_layout() - label_before_plot_selected = _translate("MainWindow_reach", "Profil précédent") - label_plot_selected = _translate("MainWindow_reach", "Profil sélectionné") - label_after_plot_selected = _translate("MainWindow_reach", "Profil suivant") + label_before_plot_selected = _translate( + "MainWindow_reach", "Profil précédent") + label_plot_selected = _translate( + "MainWindow_reach", "Profil sélectionné") + label_after_plot_selected = _translate( + "MainWindow_reach", "Profil suivant") color_before_plot_selected = "k" # 'grey' color_plot_selected = 'b' color_after_plot_selected = 'm' @@ -230,10 +241,12 @@ class PlotAC(APlot): x_gl_complete.append(station[i]) y_gl_complete.append(elevation[i]) - color_scat_complete_gl.append(self.color_complete_gl[lcomplete.index(txt)]) + color_scat_complete_gl.append( + self.color_complete_gl[lcomplete.index(txt)]) elif txt.strip() in self.incomplete_gl: annotate = self.canvas.axes.annotate( - txt, (station[i], elevation[i]), horizontalalignment='left', + txt, (station[i], elevation[i] + ), horizontalalignment='left', verticalalignment='top', annotation_clip=True, fontsize=11, color=self.color_incomplete_gl[ lincomplete.index(txt) @@ -308,10 +321,9 @@ class PlotAC(APlot): self.canvas.figure.canvas.draw_idle() - @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/Geometry/PlotKPZ.py b/src/View/Geometry/PlotKPZ.py index eb7d5e28..6825abfa 100644 --- a/src/View/Geometry/PlotKPZ.py +++ b/src/View/Geometry/PlotKPZ.py @@ -19,7 +19,7 @@ import logging from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,13 +29,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotKPZ(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): + +class PlotKPZ(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(PlotKPZ, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self.display_current = display_current @@ -61,11 +64,11 @@ class PlotKPZ(APlot): self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "Kp (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Height (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) kp = self.data.get_kp() @@ -73,7 +76,7 @@ class PlotKPZ(APlot): z_max = self.data.get_z_max() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) self.line_kp_zmin_zmax = self.canvas.axes.vlines( @@ -96,7 +99,7 @@ class PlotKPZ(APlot): ) ) - indexes_filter = lambda data: list( + def indexes_filter(data): return list( map( lambda x: x[1], filter( @@ -167,7 +170,7 @@ class PlotKPZ(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/Geometry/PlotXY.py b/src/View/Geometry/PlotXY.py index 54e056a9..9aba4652 100644 --- a/src/View/Geometry/PlotXY.py +++ b/src/View/Geometry/PlotXY.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,12 +25,16 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotXY(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, display_current=True): + +class PlotXY(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(PlotXY, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self.display_current = display_current @@ -60,18 +64,18 @@ class PlotXY(APlot): # Axes self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "X (m)"), - color='green', fontsize=12 + _translate("Geometry", "X (m)"), + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Y (m)"), - color='green', fontsize=12 + _translate("Geometry", "Y (m)"), + color='black', fontsize=10 ) self.canvas.axes.axis("equal") kp = self.data.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) # Draw line for each profile @@ -82,8 +86,8 @@ class PlotXY(APlot): markersize=3, marker='+' ) for x, y, kp in zip( - self.data.get_x(), self.data.get_y(), - kp + self.data.get_x(), self.data.get_y(), + kp ) ] @@ -137,7 +141,7 @@ class PlotXY(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return @@ -187,8 +191,10 @@ class PlotXY(APlot): self.after_plot_selected.set_visible(False) if 0 <= before < self.data.number_profiles: - self.before_plot_selected.set_data(self.data.profile(before).x(), - self.data.profile(before).y()) + self.before_plot_selected.set_data( + self.data.profile(before).x(), + self.data.profile(before).y() + ) self.before_plot_selected.set_visible(True) if 0 <= ind < self.data.number_profiles: @@ -197,8 +203,10 @@ class PlotXY(APlot): self.plot_selected.set_visible(True) if 0 <= after < self.data.number_profiles: - self.after_plot_selected.set_data(self.data.profile(after).x(), - self.data.profile(after).y()) + self.after_plot_selected.set_data( + self.data.profile(after).x(), + self.data.profile(after).y() + ) self.after_plot_selected.set_visible(True) self.canvas.axes.relim() diff --git a/src/View/Geometry/Profile/Plot.py b/src/View/Geometry/Profile/Plot.py index 076c2703..1fbeae63 100644 --- a/src/View/Geometry/Profile/Plot.py +++ b/src/View/Geometry/Profile/Plot.py @@ -19,8 +19,8 @@ import logging from tools import timer, trace -from View.Plot.APlot import APlot -from View.Plot.mpl_canvas_onpick_event import OnpickEvent +from View.Tools.PamhyrPlot import PamhyrPlot +from View.Tools.Plot.OnPickEvent import OnpickEvent from PyQt5.QtCore import ( QCoreApplication @@ -30,12 +30,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, table=None): + +class Plot(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + table=None, parent=None): super(Plot, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self._table = table @@ -60,7 +64,7 @@ class Plot(APlot): y_carto = self.data.y() if (len(x_carto) < 3 or len(y_carto) < 3 or - len(x) < 3): + len(x) < 3): # Noting to do in this case return @@ -73,11 +77,11 @@ class Plot(APlot): ) self.canvas.axes.set_xlabel( _translate("MainWindowProfile", - "Abscisse en travers (m)"), + "Transverse abscissa (m)"), color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - _translate("MainWindowProfile", "Cote (m)"), + _translate("MainWindowProfile", "Height (m)"), color='black', fontsize=10 ) diff --git a/src/View/Geometry/Profile/Table.py b/src/View/Geometry/Profile/Table.py index 2de665aa..9c8e7868 100644 --- a/src/View/Geometry/Profile/Table.py +++ b/src/View/Geometry/Profile/Table.py @@ -29,10 +29,11 @@ from PyQt5.QtWidgets import ( QMessageBox, QStyledItemDelegate, QLineEdit ) from PyQt5.QtCore import ( - QModelIndex, Qt, QAbstractTableModel, - QVariant, QCoreApplication + QModelIndex, Qt, QVariant, QCoreApplication ) +from View.Tools.PamhyrTable import PamhyrTableModel + from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.ProfileXYZ import ProfileXYZ @@ -43,43 +44,22 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(QAbstractTableModel): - def __init__(self, profile: ProfileXYZ, header=None, undo=None): - QAbstractTableModel.__init__(self) - - self._undo_stack = undo - self._profile = profile - - if header is None: - self._header = [ - "X (m)", "Y (m)", "Z (m)", - _translate("MainWindowProfile", "Nom"), - _translate("MainWindowProfile", "Abs en travers (m)") - ] - else: - self._header = header - - def rowCount(self, parent=QModelIndex()): - return self._profile.number_points - - def columnCount(self, parent=QModelIndex()): - return len(self._header) - +class GeometryProfileTableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if index.isValid(): if role == Qt.DisplayRole: value = "" if index.column() == 0: - value = self._profile.point(index.row()).x + value = self._data.point(index.row()).x elif index.column() == 1: - value = self._profile.point(index.row()).y + value = self._data.point(index.row()).y elif index.column() == 2: - value = self._profile.point(index.row()).z + value = self._data.point(index.row()).z elif index.column() == 3: - value = self._profile.point(index.row()).name + value = self._data.point(index.row()).name elif index.column() == 4: - station = self._profile.get_station() + station = self._data.get_station() if station is None: return "-" else: @@ -95,25 +75,25 @@ class TableEditableModel(QAbstractTableModel): return Qt.AlignHCenter | Qt.AlignVCenter if index.column() == 2: - value = self._profile.point(index.row()).z + value = self._data.point(index.row()).z if role == Qt.ForegroundRole: - if value == self._profile.z_min(): + if value == self._data.z_min(): return QColor("red") - elif value == self._profile.z_max(): + elif value == self._data.z_max(): return QColor("blue") if role == Qt.ToolTipRole: - if value == self._profile.z_min(): + if value == self._data.z_min(): return _translate("MainWindowProfile", "La cote du fond", "Z minimale") - elif value == self._profile.z_max(): + elif value == self._data.z_max(): return _translate("MainWindowProfile", "La cote maximale", "Z maximale") if index.column() == 3: - value = self._profile.point(index.row()).name + value = self._data.point(index.row()).name if value.strip().upper() in ["RG", "RD"]: if role == Qt.FontRole: @@ -126,24 +106,13 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.ToolTipRole: if value.strip().upper() == "RG": - return _translate("MainWindowProfile", "Rive gauche") + return _translate( + "MainWindowProfile", "Rive gauche" + ) else: - return _translate("MainWindowProfile", "Rive droite") - - return QVariant() - - def headerData(self, section, orientation, role=Qt.DisplayRole): - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self._header[section] - elif orientation == Qt.Vertical and role == Qt.DisplayRole: - return str(section + 1) - - if role == Qt.ToolTipRole and section == 4: - return _translate( - "MainWindowProfile", - "Abscisse en travers calculée en projétant les points" - " \nsur le plan défini par les deux points nommés extrêmes " - ) + return _translate( + "MainWindowProfile", "Rive droite" + ) return QVariant() @@ -154,34 +123,34 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.EditRole: try: if column == 0: - self._undo_stack.push( + self._undo.push( SetXCommand( - self._profile, row, - self._profile.point(row).x, + self._data, row, + self._data.point(row).x, value ) ) elif column == 1: - self._undo_stack.push( + self._undo.push( SetYCommand( - self._profile, row, - self._profile.point(row).y, + self._data, row, + self._data.point(row).y, value ) ) elif column == 2: - self._undo_stack.push( + self._undo.push( SetZCommand( - self._profile, row, - self._profile.point(row).z, + self._data, row, + self._data.point(row).z, value ) ) elif column == 3: - self._undo_stack.push( + self._undo.push( SetNameCommand( - self._profile, row, - self._profile.point(row).name, + self._data, row, + self._data.point(row).name, value ) ) @@ -213,9 +182,9 @@ class TableEditableModel(QAbstractTableModel): def insert_row(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) - self._undo_stack.push( + self._undo.push( AddCommand( - self._profile, row + self._data, row ) ) @@ -225,9 +194,9 @@ class TableEditableModel(QAbstractTableModel): def remove_rows(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) - self._undo_stack.push( + self._undo.push( DelCommand( - self._profile, rows + self._data, rows ) ) @@ -239,15 +208,15 @@ class TableEditableModel(QAbstractTableModel): reverse = (order != Qt.AscendingOrder) - self._undo_stack.push( + self._undo.push( SortCommand( - self._profile, column, reverse + self._data, column, reverse ) ) self.layoutChanged.emit() - def move_row_up(self, row, parent=QModelIndex()): + def move_up(self, row, parent=QModelIndex()): if row <= 0: return @@ -255,26 +224,26 @@ class TableEditableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._profile, "up", row + self._data, "up", row ) ) self.endMoveRows() self.layoutChanged.emit() - def move_row_down(self, row_to_move, parent=QModelIndex()): - if row > self._profile.number_points: + def move_down(self, row_to_move, parent=QModelIndex()): + if row > self._data.number_points: return target = row self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._profile, "down", row + self._data, "down", row ) ) @@ -282,7 +251,7 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def paste(self, row, header, data): - if row > self._profile.number_points: + if row > self._data.number_points: return if len(data) == 0: @@ -290,12 +259,12 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( PasteCommand( - self._profile, row, + self._data, row, list( map( - lambda d: self._profile.point_from_data(header, d), + lambda d: self._data.point_from_data(header, d), data ) ) @@ -306,32 +275,9 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def undo(self): - self._undo_stack.undo() + self._undo.undo() self.layoutChanged.emit() def redo(self): - self._undo_stack.redo() + self._undo.redo() self.layoutChanged.emit() - - -class Delegate(QStyledItemDelegate): - def __init__(self, parent=None, setModelDataEvent=None): - super(Delegate, self).__init__(parent) - self.setModelDataEvent = setModelDataEvent - - def createEditor(self, parent, option, index): - index.model().data(index, Qt.DisplayRole) - return QLineEdit(parent) - - def setEditorData(self, editor, index): - value = index.model().data(index, Qt.DisplayRole) - editor.setText(str(value)) - - def setModelData(self, editor, model, index): - model.setData(index, editor.text()) - - if not self.setModelDataEvent is None: - self.setModelDataEvent() - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) diff --git a/src/View/Geometry/Profile/Translate.py b/src/View/Geometry/Profile/Translate.py new file mode 100644 index 00000000..27a98c1e --- /dev/null +++ b/src/View/Geometry/Profile/Translate.py @@ -0,0 +1,37 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate +from View.Geometry.Translate import GeometryTranslate + +_translate = QCoreApplication.translate + + +class GeometryProfileTranslate(GeometryTranslate): + def __init__(self): + super(GeometryProfileTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "x": _translate("Geometry", "X (m)"), + "y": _translate("Geometry", "Y (m)"), + "z": _translate("Geometry", "Z (m)"), + "name": _translate("Geometry", "Name"), + "abs": _translate("Geometry", "Traversal abs (m)"), + } diff --git a/src/View/Geometry/Profile/UndoCommand.py b/src/View/Geometry/Profile/UndoCommand.py index 7f0cd138..e1bf3ae8 100644 --- a/src/View/Geometry/Profile/UndoCommand.py +++ b/src/View/Geometry/Profile/UndoCommand.py @@ -34,6 +34,7 @@ class SetDataCommand(QUndoCommand): self._old = old_value self._new = self.type(new_value) + class SetXCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -45,6 +46,7 @@ class SetXCommand(SetDataCommand): def redo(self): self._profile.point(self._index).x = self._new + class SetYCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -56,6 +58,7 @@ class SetYCommand(SetDataCommand): def redo(self): self._profile.point(self._index).y = self._new + class SetZCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -67,10 +70,12 @@ class SetZCommand(SetDataCommand): def redo(self): self._profile.point(self._index).z = self._new + class SetNameCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = str - super(SetNameCommand, self).__init__(reach, index, old_value, new_value) + super(SetNameCommand, self).__init__( + reach, index, old_value, new_value) def undo(self): self._profile.point(self._index).name = self._old @@ -88,7 +93,7 @@ class AddCommand(QUndoCommand): self._point = None def undo(self): - self._profile.delete([self._index]) + self._profile.delete_i([self._index]) def redo(self): if self._point is None: @@ -96,6 +101,7 @@ class AddCommand(QUndoCommand): else: self._profile.insert_point(self._index, self._point) + class DelCommand(QUndoCommand): def __init__(self, profile, rows): QUndoCommand.__init__(self) @@ -113,7 +119,8 @@ class DelCommand(QUndoCommand): self._profile.insert_point(row, point) def redo(self): - self._profile.delete(self._rows) + self._profile.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, profile, column, _reverse): @@ -174,7 +181,7 @@ class PasteCommand(QUndoCommand): def undo(self): for ind in range(len(self._points)): - self._profile.delete([self._row]) + self._profile.delete_i([self._row]) def redo(self): for point in self._points: diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 54d1e011..0b241d94 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -31,221 +31,203 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, - QUndoStack, QShortcut, + QUndoStack, QShortcut, QTableView, QAbstractItemView, + QHeaderView, QVBoxLayout, QAction, ) from Model.Geometry.Reach import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ -from View.ASubWindow import ASubMainWindow -from View.Geometry.Profile.mainwindow_ui_profile import Ui_MainWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas + from View.Geometry.Profile.Plot import Plot -from View.Geometry.Profile.Table import * +from View.Geometry.Profile.Table import GeometryProfileTableModel +from View.Geometry.Profile.Translate import GeometryProfileTranslate _translate = QCoreApplication.translate -class ProfileWindow(ASubMainWindow): - def __init__(self, profile=None, title="Profile", parent=None): - self._title = title - self.parent = parent - super(ProfileWindow, self).__init__( - name=self._title, - parent=self.parent - ) - - self.ui = Ui_MainWindow() - self.ui.setupUi(self) +class ProfileWindow(PamhyrWindow): + _pamhyr_ui = "GeometryCrossSection" + _pamhyr_name = "Geometry cross-section" + def __init__(self, profile=None, study=None, config=None, parent=None): self._profile = profile - self._model = None - self.setup_window() - self.setup_sc() - self.setup_model() + name = f"{self._pamhyr_name} - {self._profile.name} {self._profile.kp}" + super(ProfileWindow, self).__init__( + title=name, + study=study, + config=config, + trad=GeometryProfileTranslate(), + parent=parent + ) + + self._hash_data.append(profile) + + self.setup_table() + self.setup_plot() self.setup_connections() - self.plot() - self._model.dataChanged.connect(self.update_plot) - self.fileName = None + def setup_table(self): + table_headers = self._trad.get_dict("table_headers") - # self.ui.tableView.installEventFilter(self) - # self._model.dataChanged.connect(self.tableview_is_modified) - - # self.ui.btn_go_back.setEnabled(False) - # self.ui.btn_check.setEnabled(False) - # self._model.dataChanged.connect(self.set_enable_cancel_btn) - # self._model.dataChanged.connect(self.set_enable_validate_changes_btn) - # self.ui.btn_reset.setEnabled(False) - # self._model.dataChanged.connect(self.set_enable_go_back_initial_state_btn) - - def setup_window(self): - header = _translate("MainWindowProfile", "Profile") - - name = self._profile.name - if (name is None) or (name == ""): - name = _translate("MainWindowProfile", "(no name)") - - self._title = ( - header + " - " + - f"{self._profile.reach.name}" + " - " + - f"{name} ({self._profile.kp})" + table = self.find(QTableView, "tableView") + self._tablemodel = GeometryProfileTableModel( + table_view=table, + table_headers=table_headers, + editable_headers=["name", "x", "y", "z"], + data=self._profile, + undo=self._undo_stack ) + table.setModel(self._tablemodel) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) - self.setWindowTitle(self._title) + def setup_plot(self): + self._tablemodel.blockSignals(True) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_model(self): - self._model = TableEditableModel( - profile = self._profile, - undo = self._undo_stack + self._canvas = MplCanvas(width=3, height=4, dpi=100) + self._canvas.setObjectName("canvas") + self._toolbar = PamhyrPlotToolbar( + self._canvas, self, + items=["home", "zoom", "save", "iso", "back/forward", "move"] ) - - self.ui.tableView.setModel(self._model) - self.ui.tableView.setItemDelegate(Delegate()) - - def setup_connections(self): - self.ui.btn_sort_asc_x.triggered.connect(self.sort_X_ascending) - self.ui.btn_sort_desc_x.triggered.connect(self.sort_X_descending) - self.ui.btn_sort_asc_y.triggered.connect(self.sort_Y_ascending) - self.ui.btn_sort_desc_y.triggered.connect(self.sort_Y_descending) - self.ui.btn_move_up.triggered.connect(self.move_row_up) - self.ui.btn_move_down.triggered.connect(self.move_row_down) - self.ui.btn_export.triggered.connect(self.handleSave) - self.ui.btn_add.triggered.connect(self.insert_row) - self.ui.btn_delete.triggered.connect(self.delete_row) - # self.ui.btn_copy.clicked.connect(self.copyTable) - # self.ui.btn_paste.clicked.connect(self.pasteTable) - # self.ui.btn_check.clicked.connect(self.validate_changes) - # self.ui.btn_go_back.clicked.connect(self.cancel_validate_changes) - # self.ui.btn_reset.clicked.connect(self.go_back_to_initial_state) - - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - - def plot(self): - self.ui.tableView.model().blockSignals(True) + self._plot_layout = self.find(QVBoxLayout, "verticalLayout") + self._plot_layout.addWidget(self._toolbar) + self._plot_layout.addWidget(self._canvas) self._plot = Plot( - canvas = self.ui.canvas, - data = self._profile, - toolbar = None, - table = self.ui.tableView, + canvas=self._canvas, + data=self._profile, + toolbar=self._toolbar, + table=self.find(QTableView, "tableView") ) self._plot.draw() - self.ui.tableView.model().blockSignals(False) + self._tablemodel.blockSignals(False) + + def setup_connections(self): + actions = { + "action_sort_asc": self.sort_X_ascending, + "action_sort_des": self.sort_X_descending, + "action_up": self.move_up, + "action_down": self.move_down, + "action_add": self.add, + "action_delete": self.delete, + } + + for action in actions: + self.find(QAction, action)\ + .triggered.connect(actions[action]) + + self._tablemodel.dataChanged.connect(self.update_plot) def update_plot(self): - self.ui.tableView.model().blockSignals(True) + self._tablemodel.blockSignals(True) # TODO: Do not rebuild all graph self._plot.update() - self.ui.tableView.model().blockSignals(False) + self._tablemodel.blockSignals(False) def index_selected_row(self): - rows = self.ui.tableView\ - .selectionModel()\ - .selectedRows() + table = self.find(QTableView, "tableView") + rows = table.selectionModel()\ + .selectedRows() if len(rows) == 0: return 0 - return self.ui.tableView\ - .selectionModel()\ - .selectedRows()[0]\ - .row() + return rows[0].row() - def insert_row(self): - if len(self.ui.tableView.selectedIndexes()) == 0: - self._model.insert_row(self._model.rowCount()) + def add(self): + table = self.find(QTableView, "tableView") + if len(table.selectedIndexes()) == 0: + self._tablemodel.insert_row(self._tablemodel.rowCount()) else: row = self.index_selected_row() - self._model.insert_row(row + 1) + self._tablemodel.insert_row(row + 1) self.update_plot() - def delete_row(self): + def delete(self): + table = self.find(QTableView, "tableView") rows = sorted( list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in table.selectedIndexes()] ) ) ) if len(rows) > 0: - self._model.remove_rows(rows) + self._tablemodel.remove_rows(rows) self.update_plot() def sort_X_ascending(self): - self._model.sort('x', order=Qt.AscendingOrder) + self._tablemodel.sort('x', order=Qt.AscendingOrder) self.update_plot() def sort_X_descending(self): - self._model.sort('x', order=Qt.DescendingOrder) + self._tablemodel.sort('x', order=Qt.DescendingOrder) self.update_plot() def sort_Y_ascending(self): - self._model.sort('y', order=Qt.AscendingOrder) + self._tablemodel.sort('y', order=Qt.AscendingOrder) self.update_plot() def sort_Y_descending(self): - self._model.sort('y', order=Qt.DescendingOrder) + self._tablemodel.sort('y', order=Qt.DescendingOrder) self.update_plot() - def move_row_down(self): + def move_down(self): rows = list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in + self.find(QTableView, "tableView").selectedIndexes()] ) ) for row in rows: - if row < self._model.rowCount() - 1: - self._model.move_row_down(row) + if row < self._tablemodel.rowCount() - 1: + self._tablemodel.move_down(row) self.update_plot() - def move_row_up(self): + def move_up(self): rows = list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in + self.find(QTableView, "tableView").selectedIndexes()] ) ) for row in rows: if 0 < row: - self._model.move_row_up(row) + self._tablemodel.move_up(row) self.update_plot() - def copy(self): - rows = self.ui.tableView\ - .selectionModel()\ - .selectedRows() - table = [] - table.append(["x", "y", "z", "name"]) + def _copy(self): + table = self.find(QTableView, "tableView") + rows = table.selectionModel().selectedRows() + + data = [] + data.append(["x", "y", "z", "name"]) for row in rows: point = self._profile.point(row.row()) - table.append( + data.append( [ point.x, point.y, point.z, point.name ] ) - self.copyTableIntoClipboard(table) + self.copyTableIntoClipboard(data) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -257,136 +239,13 @@ class ProfileWindow(ASubMainWindow): row.append(self._profile) row = self.index_selected_row() - self._model.paste(row, header, data) + self._tablemodel.paste(row, header, data) self.update_plot() - def undo(self): - self._model.undo() + def _undo(self): + self._tablemodel.undo() self.update_plot() - def redo(self): - self._model.redo() + def _redo(self): + self._tablemodel.redo() self.update_plot() - - def handleSave(self): - if self.fileName is None or self.fileName == '': - self.fileName, self.filters = QFileDialog.getSaveFileName( - self, filter="CSV files (*.csv)" - ) - - if self.fileName != '': - with open(self.fileName, 'w') as stream: - csvout = csv.writer(stream, delimiter='\t', quotechar=' ', - escapechar=None, - quoting=csv.QUOTE_NONNUMERIC, - lineterminator='\n') - - for row in range(self._model.rowCount(QModelIndex())): - rowdata = [] - for column in range(self._model.columnCount(QModelIndex())): - item = self._model.index( - row, column, QModelIndex() - ).data(Qt.DisplayRole) - - if item is not None: - rowdata.append(item) - - if item == 'nan': - rowdata.remove(item) - - csvout.writerow(rowdata) - - def handleOpen(self): - self.fileName, self.filterName = QFileDialog.getOpenFileName(self) - - if self.fileName != '': - with open(self.fileName, 'r') as f: - reader = csv.reader(f, delimiter='\t') - header = next(reader) - - buf = [] - for row in reader: - row[0] = QCheckBox("-") - buf.append(row) - - self._model = None - self._model = TableEditableModel(buf) - self.ui.tableView.setModel(self._model) - self.fileName = '' - - def set_enable_validate_changes_btn(self): - self.ui.btn_check.setEnabled(True) - - def set_enable_cancel_btn(self): - self.ui.btn_go_back.setEnabled(True) - - def set_enable_go_back_initial_state_btn(self): - self.ui.btn_reset.setEnabled(True) - - def delete_empty_rows(self): - if self._model.data_contains_nan(): - buttonReply = QtWidgets.QMessageBox.question( - self, - _translate("MainWindowProfile", - "Suppression les lignes incomplètes"), - _translate("MainWindowProfile", - "Supprimer les lignes des cellules" - " non renseignées ?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - - if buttonReply == QtWidgets.QMessageBox.Yes: - self._model.delete_empty_rows() - - if buttonReply == QtWidgets.QMessageBox.No: - pass - - def remove_duplicates_point_names(self): - counter_list = [] - list_deleted_names = [] - - for ind, name_point in enumerate(self._model.name): - if name_point not in counter_list: - counter_list.append(name_point) - elif len(name_point.strip()) > 0 and name_point in counter_list: - if name_point not in list_deleted_names: - list_deleted_names.append(name_point) - - if list_deleted_names: - self.msg_box_check_duplication_names(list_deleted_names) - - def msg_box_check_duplication_names(self, list_deleted_names): - if len(list_deleted_names) == 1: - text = _translate("MainWindowProfile", - "Le nom ''{}'' est dupliqué." - " \n\nYes : Ne garder que la première occurrence. \nNo :" - " Annuler la suppression.".format(list_deleted_names[0])) - else: - text = _translate("MainWindowProfile", - "Les noms suivants : \n{} sont dupliqués" - " \n\nYes : Ne garder que la première occurrence de " - "chaque nom. \nNo :" - " Annuler la suppression.".format(list_deleted_names)) - - buttonReply = QtWidgets.QMessageBox.question( - self, _translate("MainWindowProfile", - "Suppression des noms répétés"), - text, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - - if buttonReply == QtWidgets.QMessageBox.Yes: - self._model.remove_duplicates_names() - - def ask_quit(self): - choice = QtWidgets.QMessageBox.question( - self, - _translate("MainWindowProfile", "Quittez ?"), - _translate("MainWindowProfile", - "Etes-vous sûr de vouloir quitter ?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - return choice == QtWidgets.QMessageBox.Yes - - def initial_state_model(self, profile_initial_state): - return profile_initial_state diff --git a/src/View/Geometry/Profile/mainwindow_ui_profile.py b/src/View/Geometry/Profile/mainwindow_ui_profile.py deleted file mode 100644 index a6844e55..00000000 --- a/src/View/Geometry/Profile/mainwindow_ui_profile.py +++ /dev/null @@ -1,260 +0,0 @@ -# mainwindow_ui_profile.py -- Pamhyr -# Copyright (C) 2023 INRAE -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# -*- coding: utf-8 -*- - -import os.path - -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import Qt, QSize -from PyQt5.QtWidgets import ( - QTableView, QAbstractItemView, QHeaderView, - QToolBar, -) - -from View.Plot import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar - -file_path = os.path.abspath(os.path.dirname(__file__)) -icon_path = f"{os.path.dirname(os.path.dirname(file_path))}/../View/ui/ressources/" - -class Ui_MainWindow(object): - def setupUi(self, MainWindowProfile): - MainWindowProfile.setObjectName("MainWindowProfile") - MainWindowProfile.resize(1089, 699) - self.centralwidget = QtWidgets.QWidget(MainWindowProfile) - self.centralwidget.setObjectName("centralwidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.splitter = QtWidgets.QSplitter(self.centralwidget) - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.widget = QtWidgets.QWidget(self.splitter) - self.widget.setObjectName("widget") - self.verticalLayout_left = QtWidgets.QVBoxLayout(self.widget) - self.verticalLayout_left.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_left.setObjectName("verticalLayout_left") - - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - - self.toolBar = QToolBar(MainWindowProfile) - self.toolBar.setOrientation(Qt.Vertical) - self.toolBar.setEnabled(True) - MainWindowProfile.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) - - btn_size = QSize(40, 28) - - self.btn_add = QtWidgets.QAction(self.widget) - self.btn_add.setObjectName("btn_add") - icon_btn_add = QtGui.QIcon() - icon_btn_add.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-add.png")) - self.btn_add.setIcon(icon_btn_add) - self.toolBar.addAction(self.btn_add) - - self.btn_delete = QtWidgets.QAction(self.widget) - self.btn_delete.setObjectName("btn_delete") - icon_btn_delete = QtGui.QIcon() - icon_btn_delete.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-remove.png")) - self.btn_delete.setIcon(icon_btn_delete) - self.toolBar.addAction(self.btn_delete) - - self.btn_sort_asc_x = QtWidgets.QAction(self.widget) - self.btn_sort_asc_x.setObjectName("btn_sort_asc_x") - icon_btn_sort_asc_x = QtGui.QIcon() - icon_btn_sort_asc_x.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc_x.setIcon(icon_btn_sort_asc_x) - self.toolBar.addAction(self.btn_sort_asc_x) - - self.btn_sort_desc_x = QtWidgets.QAction(self.widget) - self.btn_sort_desc_x.setObjectName("btn_sort_desc_x") - icon_btn_sort_desc_x = QtGui.QIcon() - icon_btn_sort_desc_x.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc_x.setIcon(icon_btn_sort_desc_x) - self.toolBar.addAction(self.btn_sort_desc_x) - - self.btn_sort_asc_y = QtWidgets.QAction(self.widget) - self.btn_sort_asc_y.setObjectName("btn_sort_asc_y") - icon_btn_sort_asc_y = QtGui.QIcon() - icon_btn_sort_asc_y.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc_y.setIcon(icon_btn_sort_asc_y) - self.toolBar.addAction(self.btn_sort_asc_y) - - self.btn_sort_desc_y = QtWidgets.QAction(self.widget) - self.btn_sort_desc_y.setObjectName("btn_sort_desc_y") - icon_btn_sort_desc_y = QtGui.QIcon() - icon_btn_sort_desc_y.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc_y.setIcon(icon_btn_sort_desc_y) - self.toolBar.addAction(self.btn_sort_desc_y) - - self.btn_move_up = QtWidgets.QAction(self.widget) - self.btn_move_up.setObjectName("btn_move_up") - icon_btn_move_up = QtGui.QIcon() - icon_btn_move_up.addPixmap( - QtGui.QPixmap(f"{icon_path}up.png")) - self.btn_move_up.setIcon(icon_btn_move_up) - self.toolBar.addAction(self.btn_move_up) - - self.btn_move_down = QtWidgets.QAction(self.widget) - self.btn_move_down.setObjectName("btn_move_down") - icon_btn_move_down = QtGui.QIcon() - icon_btn_move_down.addPixmap( - QtGui.QPixmap(f"{icon_path}down.png")) - self.btn_move_down.setIcon(icon_btn_move_down) - self.toolBar.addAction(self.btn_move_down) - - self.btn_export = QtWidgets.QAction(self.widget) - self.btn_export.setObjectName("btn_export") - icon_btn_export = QtGui.QIcon() - icon_btn_export.addPixmap( - QtGui.QPixmap(f"{icon_path}export.png")) - self.btn_export.setIcon(icon_btn_export) - self.toolBar.addAction(self.btn_export) - - self.btn_copy = QtWidgets.QAction(self.widget) - self.btn_copy.setObjectName("btn_copy") - icon_btn_copy = QtGui.QIcon() - icon_btn_copy.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-copy.png")) - self.btn_copy.setIcon(icon_btn_copy) - self.toolBar.addAction(self.btn_copy) - - self.btn_paste = QtWidgets.QAction(self.widget) - self.btn_paste.setObjectName("btn_paste") - icon_btn_paste = QtGui.QIcon() - icon_btn_paste.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-paste.png")) - self.btn_paste.setIcon(icon_btn_paste) - self.toolBar.addAction(self.btn_paste) - - self.btn_check = QtWidgets.QAction(self.widget) - self.btn_check.setObjectName("btn_check") - icon_btn_check = QtGui.QIcon() - icon_btn_check.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-apply.png")) - self.btn_check.setIcon(icon_btn_check) - self.toolBar.addAction(self.btn_check) - - self.btn_go_back = QtWidgets.QAction(self.widget) - self.btn_go_back.setObjectName("btn_go_back") - icon_btn_go_back = QtGui.QIcon() - icon_btn_go_back.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-undo.png")) - self.btn_go_back.setIcon(icon_btn_go_back) - self.toolBar.addAction(self.btn_go_back) - - self.btn_reset = QtWidgets.QAction(self.widget) - self.btn_reset.setObjectName("btn_reset") - icon_btn_reset = QtGui.QIcon() - icon_btn_reset.addPixmap( - QtGui.QPixmap(f"{icon_path}cancel.png")) - self.btn_reset.setIcon(icon_btn_reset) - self.toolBar.addAction(self.btn_reset) - - self.verticalLayout_left.addLayout(self.horizontalLayout) - - self.tableView = QTableView(self.widget) - self.tableView.setObjectName("tableView") - self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) - self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) - self.tableView.resizeColumnsToContents() - self.tableView.resizeRowsToContents() - self.tableView.setAlternatingRowColors(True) - self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.verticalLayout_left.addWidget(self.tableView) - - self.widget1 = QtWidgets.QWidget(self.splitter) - self.widget1.setObjectName("widget1") - - self.verticalLayout_right = QtWidgets.QVBoxLayout(self.widget1) - self.verticalLayout_right.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_right.setObjectName("verticalLayout_right") - - self.label_graphic_help = QtWidgets.QLabel(self.widget1) - self.label_graphic_help.setMaximumSize(QtCore.QSize(16777215, 25)) - self.label_graphic_help.setBaseSize(QtCore.QSize(0, 30)) - self.label_graphic_help.setObjectName("label_graphic_help") - self.verticalLayout_right.addWidget(self.label_graphic_help) - - self.canvas = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas.setObjectName("canvas") - - self.my_toolbar = PamhyrPlotToolbar( - self.canvas, self.widget1 - ) - - self.verticalLayout_right.addWidget(self.my_toolbar) - self.verticalLayout_right.addWidget(self.canvas) - self.horizontalLayout_2.addWidget(self.splitter) - - MainWindowProfile.setCentralWidget(self.centralwidget) - - self.menubar = QtWidgets.QMenuBar(MainWindowProfile) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1089, 20)) - self.menubar.setObjectName("menubar") - MainWindowProfile.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindowProfile) - self.statusbar.setObjectName("statusbar") - MainWindowProfile.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindowProfile) - QtCore.QMetaObject.connectSlotsByName(MainWindowProfile) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindowProfile", "MainWindowProfile")) - self.btn_add.setToolTip(_translate("MainWindowProfile", "Insérer un point")) - self.btn_delete.setToolTip( - _translate("MainWindowProfile", "Supprimer le/les point(s) sélectionnés")) - self.btn_sort_asc_x.setToolTip( - _translate("MainWindowProfile", "Trier les points par ordre croissant de X")) - self.btn_sort_desc_x.setToolTip( - _translate("MainWindowProfile", "Trier les points par ordre décroissant de X")) - self.btn_sort_asc_y.setToolTip(_translate("MainWindowProfile", - "Trier les points par ordre croissant de Y" - )) - self.btn_sort_desc_y.setToolTip(_translate("MainWindowProfile", - "Trier les points par ordre décroissant de Y")) - self.btn_move_up.setToolTip(_translate("MainWindowProfile", - "Décaler le point sélectionné vers le haut")) - self.btn_move_down.setToolTip(_translate("MainWindowProfile", - "Décaler le point sélectionné vers le bas")) - self.btn_export.setToolTip(_translate("MainWindowProfile", - "Exporter (dans un fichier) les points du profil au format tabulé")) - self.btn_copy.setToolTip( - _translate("MainWindowProfile", "Copier la sélection au format tabulé")) - self.btn_paste.setToolTip(_translate("MainWindowProfile", - "Coller la sélection depuis le presse-papier au format tabulé")) - self.btn_check.setToolTip(_translate("MainWindowProfile", - "Vérifier la validité de la saisie et garder ou pas les modifications " - "apportées")) - self.btn_go_back.setToolTip(_translate("MainWindowProfile", - "Annuler toutes les modifications depuis la dernière validation")) - self.btn_reset.setToolTip(_translate("MainWindowProfile", - "Annuler toutes les modifications et revenir à l\'état initial")) - self.label_graphic_help.setToolTip(_translate("MainWindowProfile", - "Ligne d'eau : \n \tZ : Cote (m) \n \tA : Aire mouillée (" - "m\u00B2) \n \tp : " - "Périmètre mouillé (m) \n \tL : Largeur au miroir (m)")) - self.label_graphic_help.setText( - _translate("MainWindowProfile", "\'Maj + Clic\' : Ligne d\'eau & \'Ctrl + Clic\' : " - "Sélectionner des points")) diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 1b055209..2abb2e71 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -35,6 +35,8 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from Model.Geometry import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ from View.Geometry.UndoCommand import * @@ -44,57 +46,30 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(QAbstractTableModel): - def __init__(self, reach, headers=None, undo=None): - QAbstractTableModel.__init__(self) - data_list = [] - - self._undo_stack = undo - self._reach = reach - - # Hack for qtlinguist - _ = _translate("Geometry", "Name") - _ = _translate("Geometry", "Kp (m)") - _ = _translate("Geometry", "Type") - - if headers is None: - self.headers = [ - "Name", - "Kp (m)", - "Type" - ] - else: - self.headers = headers - - def rowCount(self, parent=QModelIndex()): - return self._reach.number_profiles - - def columnCount(self, parent=QModelIndex()): - return len(self.headers) - +class GeometryReachTableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() if role == Qt.DisplayRole and index.column() == 0: - return self._reach.profile(index.row()).name + return self._data.profile(index.row()).name if role == Qt.DisplayRole and index.column() == 1: - kp = self._reach.profile(index.row()).kp + kp = self._data.profile(index.row()).kp return f"{kp:.4f}" if role == Qt.DisplayRole and index.column() == 2: - return self._reach.profile(index.row()).profile_type + return self._data.profile(index.row()).profile_type if role == Qt.TextAlignmentRole: return Qt.AlignHCenter | Qt.AlignVCenter if role == Qt.ForegroundRole and index.column() == 0: - name = self._reach.profile(index.row()).name\ - .strip()\ - .lower() + name = self._data.profile(index.row()).name\ + .strip()\ + .lower() if (name == "upstream" or name == "up" or - name == _translate("Geometry", "upstream")): + name == _translate("Geometry", "upstream")): return QColor("Green") elif (name == "downstream" or name == "down" or name == _translate("Geometry", "downstream")): @@ -102,16 +77,6 @@ class TableEditableModel(QAbstractTableModel): return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.DisplayRole: - if orientation == Qt.Horizontal: - if section < len(self.headers): - return _translate("Geometry", self.headers[section]) - else: - return str(section + 1) - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): row = index.row() column = index.column() @@ -119,20 +84,20 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.EditRole and index.column() != 2: try: if index.column() == 0: - self._undo_stack.push( + self._undo.push( SetNameCommand( - self._reach, index.row(), - self._reach.profile(index.row()).name, + self._data, index.row(), + self._data.profile(index.row()).name, value ) ) if index.column() == 1: - self._undo_stack.push( + self._undo.push( SetKPCommand( - self._reach, index.row(), - self._reach.profile(index.row()).kp, - value + self._data, index.row(), + self._data.profile(index.row()).kp, + value ) ) except Exception as e: @@ -149,39 +114,25 @@ class TableEditableModel(QAbstractTableModel): return False - def index(self, row, column, parent=QModelIndex()): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - return self.createIndex(row, column, QModelIndex()) - - def flags(self, index): - flg = Qt.ItemIsEnabled | Qt.ItemIsSelectable - - if index.column() == 2: - return flg - else: - return Qt.ItemIsEditable | flg - # @QtCore.pyqtSlot() - def insert_row(self, row, parent=QModelIndex()): + def add(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) - self._undo_stack.push( + self._undo.push( AddCommand( - self._reach, row + self._data, row ) ) self.endInsertRows() self.layoutChanged.emit() - def remove_rows(self, rows, parent=QModelIndex()): + def delete(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) - self._undo_stack.push( + self._undo.push( DelCommand( - self._reach, rows + self._data, rows ) ) @@ -191,17 +142,16 @@ class TableEditableModel(QAbstractTableModel): def sort_profiles(self, _reverse): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( SortCommand( - self._reach, _reverse + self._data, _reverse ) ) self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - - def move_row_up(self, row, parent=QModelIndex()): + def move_up(self, row, parent=QModelIndex()): if row <= 0: return @@ -209,26 +159,26 @@ class TableEditableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._reach, "up", row + self._data, "up", row ) ) self.endMoveRows() self.layoutChanged.emit() - def move_row_down(self, row, parent=QModelIndex()): - if row > self._reach.number_profiles: + def move_down(self, row, parent=QModelIndex()): + if row > self._data.number_profiles: return target = row self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._reach, "down", row + self._data, "down", row ) ) @@ -238,9 +188,9 @@ class TableEditableModel(QAbstractTableModel): def duplicate(self, rows, profiles): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( DuplicateCommand( - self._reach, rows, + self._data, rows, profiles ) ) @@ -248,9 +198,8 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - def paste(self, row, header, data): - if row > self._reach.number_profiles: + if row > self._data.number_profiles: return if len(data) == 0: @@ -258,9 +207,9 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( PasteCommand( - self._reach, row, + self._data, row, list( map( lambda d: ProfileXYZ.from_data(header, d), @@ -272,71 +221,3 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - - - def undo(self): - self._undo_stack.undo() - self.layoutChanged.emit() - - def redo(self): - self._undo_stack.redo() - self.layoutChanged.emit() - - -class Delegate(QStyledItemDelegate): - def __init__(self, parent=None, setModelDataEvent=None): - super(Delegate, self).__init__(parent) - self.setModelDataEvent = setModelDataEvent - - def createEditor(self, parent, option, index): - index.model().data(index, Qt.DisplayRole) - return QLineEdit(parent) - - def setEditorData(self, editor, index): - value = index.model().data(index, Qt.DisplayRole) - editor.setText(str(value)) - - def setModelData(self, editor, model, index): - model.setData(index, editor.text()) - - if not self.setModelDataEvent is None: - self.setModelDataEvent() - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) - -class Delegate1(QStyledItemDelegate): - def __init__(self, owner, choices): - super().__init__(owner) - self.items = choices - - def paint(self, painter, option, index): - if isinstance(self.parent(), QAbstractItemView): - self.parent().openPersistentEditor(index) - super(Delegate1, self).paint(painter, option, index) - - def createEditor(self, parent, option, index): - editor = QComboBox(parent) - # editor.currentIndexChanged.connect(self.commit_editor) - editor.addItems(self.items) - return editor - - def setEditorData(self, editor, index): - editor.blockSignals(True) - text = index.model().data(index, Qt.DisplayRole) - try: - i = self.items.index(text) - except ValueError: - i = 0 - editor.setCurrentIndex(i) - editor.blockSignals(False) - - def setModelData(self, editor, model, index): - model.setData(index, editor.currentText(), Qt.DisplayRole) - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) - - @pyqtSlot() - def currentIndexChanged(self): - self.commitData.emit(self.sender()) diff --git a/src/View/Geometry/Translate.py b/src/View/Geometry/Translate.py new file mode 100644 index 00000000..8ad7e2de --- /dev/null +++ b/src/View/Geometry/Translate.py @@ -0,0 +1,48 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class GeometryTranslate(PamhyrTranslate): + def __init__(self): + super(GeometryTranslate, self).__init__() + + self._dict["open_file"] = _translate("Geometry", "Open a file") + self._dict["file_st"] = _translate( + "Geometry", "File mage geometry (*.ST)") + self._dict["file_m"] = _translate( + "Geometry", "File mage meshed geometry (*.M)") + self._dict["file_all"] = _translate("Geometry", "All file (*)") + + self._dict["reach"] = _translate("Geometry", "reach") + self._dict["reachs"] = _translate("Geometry", "reachs") + + self._dict["cross_section"] = _translate("Geometry", "cross-section") + self._dict["cross_sections"] = _translate("Geometry", "cross-sections") + self._dict["profile"] = _translate("Geometry", "cross-section") + self._dict["profiles"] = _translate("Geometry", "cross-sections") + + self._sub_dict["table_headers"] = { + "name": _translate("Geometry", "Name"), + "kp": _translate("Geometry", "KP"), + } diff --git a/src/View/Geometry/UndoCommand.py b/src/View/Geometry/UndoCommand.py index 1df9c58c..147ce231 100644 --- a/src/View/Geometry/UndoCommand.py +++ b/src/View/Geometry/UndoCommand.py @@ -35,10 +35,12 @@ class SetDataCommand(QUndoCommand): self._old = old_value self._new = self.type(new_value) + class SetNameCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = str - super(SetNameCommand, self).__init__(reach, index, old_value, new_value) + super(SetNameCommand, self).__init__( + reach, index, old_value, new_value) def undo(self): self._reach.profile(self._index).name = self._old @@ -46,6 +48,7 @@ class SetNameCommand(SetDataCommand): def redo(self): self._reach.profile(self._index).name = self._new + class SetKPCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -75,6 +78,7 @@ class AddCommand(QUndoCommand): else: self._reach.insert_profile(self._index, self._profile) + class DelCommand(QUndoCommand): def __init__(self, reach, rows): QUndoCommand.__init__(self) @@ -94,6 +98,7 @@ class DelCommand(QUndoCommand): def redo(self): self._reach.delete(self._rows) + class SortCommand(QUndoCommand): def __init__(self, reach, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 1c8cb5da..4f1e0b22 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -34,117 +34,175 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, - QUndoStack, QShortcut, + QUndoStack, QShortcut, QTableView, QHeaderView, + QAction, QSlider, QPushButton, QVBoxLayout, + QLabel, QAbstractItemView, ) -from View.Geometry.PlotXY import PlotXY -from View.Geometry.PlotKPZ import PlotKPZ -from View.Geometry.PlotAC import PlotAC +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas -from View.ASubWindow import ASubMainWindow, WindowToolKit -from View.ListedSubWindow import ListedSubWindow -from View.Geometry.mainwindow_ui_reach import Ui_MainWindow -from View.Geometry.Table import * +from View.Geometry.Table import GeometryReachTableModel +from View.Geometry.PlotXY import PlotXY +from View.Geometry.PlotAC import PlotAC +from View.Geometry.PlotKPZ import PlotKPZ +from View.Geometry.Translate import GeometryTranslate from View.Geometry.Profile.Window import ProfileWindow _translate = QCoreApplication.translate -class GeometryWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, model=None, title="Geometry", parent=None): - self._title = title - self.parent = parent +class GeometryWindow(PamhyrWindow): + _pamhyr_ui = "GeometryReach" + _pamhyr_name = "Geometry" + + def __init__(self, reach=None, study=None, config=None, parent=None): + if reach is None: + self._reach = study.river.current_reach().reach + else: + self._reach = reach + + name = f"{self._pamhyr_name} - {self._reach.name}" + super(GeometryWindow, self).__init__( - name=self._title, + title=name, + study=study, + config=config, + trad=GeometryTranslate(), parent=parent ) - self._model = model - self._reach = model.river.current_reach().reach - - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - - self.tableView = self.ui.tableView - self.tableView_header = self.ui.tableView_header + # Add reach to hash computation data + self._hash_data.append(self._reach) self._tablemodel = None self._profile_window = [] - self._clipboard = None - self.setup_window() - self.setup_sc() - self.setup_model() + self.setup_table() self.setup_plots() + self.setup_statusbar() self.setup_connections() self.changed_slider_value() - def setup_window(self): - self._title = f"{self.ui.mainwindow_title} - {self._reach.name}" - self.setWindowTitle(self._title) + def setup_table(self): + table_headers = self._trad.get_dict("table_headers") - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_model(self): - self._tablemodel = TableEditableModel( - headers = self.ui.tableView_header, - reach = self._reach, - undo = self._undo_stack + table = self.find(QTableView, "tableView") + self._tablemodel = GeometryReachTableModel( + table_view=table, + table_headers=table_headers, + editable_headers=["name", "kp"], + data=self._reach, + undo=self._undo_stack ) - self.tableView.setModel(self._tablemodel) - self.tableView.setItemDelegate(Delegate()) + table.setModel(self._tablemodel) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) def setup_plots(self): + self._canvas_xy = MplCanvas(width=3, height=4, dpi=100) + self._canvas_xy.setObjectName("canvas_xy") + self._toolbar_xy = PamhyrPlotToolbar( + self._canvas_xy, self, + items=["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_xy = self.find(QVBoxLayout, "verticalLayout") + self._plot_layout_xy.addWidget(self._toolbar_xy) + self._plot_layout_xy.addWidget(self._canvas_xy) self.plot_xy() + + self._canvas_kpc = MplCanvas(width=6, height=4, dpi=100) + self._canvas_kpc.setObjectName("canvas_kpc") + self._toolbar_kpc = PamhyrPlotToolbar( + self._canvas_kpc, self, + items=["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_kpc = self.find(QVBoxLayout, "verticalLayout_2") + self._plot_layout_kpc.addWidget(self._toolbar_kpc) + self._plot_layout_kpc.addWidget(self._canvas_kpc) self.plot_kpc() + + self._canvas_ac = MplCanvas(width=9, height=4, dpi=100) + self._canvas_ac.setObjectName("canvas_ac") + self._toolbar_ac = PamhyrPlotToolbar( + self._canvas_ac, self, + items=["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_ac = self.find(QVBoxLayout, "verticalLayout_3") + self._plot_layout_ac.addWidget(self._toolbar_ac) + self._plot_layout_ac.addWidget(self._canvas_ac) self.plot_ac() + def _compute_status_label(self): + row = self.index_selected_row() + profile = self._reach.profile(row) + + name = profile.name + " " + str(profile.kp) + + return ( + "" + + f"{self._trad['reach']}: {self._reach.name}" + + " - " + + f"{self._trad['cross_section']}: {name}" + ) + + def setup_statusbar(self): + txt = "" + self._status_label = QLabel(txt) + self.statusbar.addPermanentWidget(self._status_label) + + def update_statusbar(self): + txt = self._compute_status_label() + self._status_label.setText(txt) + def setup_connections(self): - self.ui.btn_open.triggered.connect(self.open_file_dialog) - self.ui.btn_sort_asc.triggered.connect(self.sort_ascending) - self.ui.btn_sort_desc.triggered.connect(self.sort_descending) - self.ui.btn_move_up.triggered.connect(self.move_row_up) - self.ui.btn_move_down.triggered.connect(self.move_row_down) - # self.ui.btn_end_editing.triggered.connect(self.handleSave) - self.ui.btn_add.triggered.connect(self.insert_row) - self.ui.btn_delete.triggered.connect(self.delete_rows) - self.ui.btn_edit.triggered.connect(self.edit_profile) - self.ui.verticalSlider.valueChanged.connect(self.changed_slider_value) + actions = { + "action_import": self.import_from_file, + # "action_export": self.export_to_file, + "action_sort_asc": self.sort_ascending, + "action_sort_des": self.sort_descending, + "action_up": self.move_up, + "action_down": self.move_down, + "action_add": self.add, + "action_delete": self.delete, + "action_edit": self.edit_profile, + } - self.ui.btn_slider_up.clicked.connect(self.decrement_value_slider) - self.ui.btn_slider_down.clicked.connect(self.increment_value_slider) - self.ui.btn_move_up.triggered.connect(self.changed_profile_slot) + for action in actions: + self.find(QAction, action)\ + .triggered.connect(actions[action]) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) + self.find(QSlider, "verticalSlider").valueChanged.connect( + self.changed_slider_value) + self.find(QPushButton, "pushButton_up").clicked.connect( + self.decrement_value_slider) + self.find(QPushButton, "pushButton_down").clicked.connect( + self.increment_value_slider) # Profile selection when line change in table - self.tableView.selectionModel()\ - .selectionChanged\ - .connect(self.select_current_profile) + self.find(QTableView, "tableView").selectionModel()\ + .selectionChanged\ + .connect(self.select_current_profile) - def open_file_dialog(self): + def import_from_file(self): options = QFileDialog.Options() - settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) + settings = QSettings(QSettings.IniFormat, + QSettings.UserScope, 'MyOrg', ) options |= QFileDialog.DontUseNativeDialog + file_types = [ + self._trad["file_st"], + self._trad["file_m"], + self._trad["file_all"], + ] + filename, _ = QtWidgets.QFileDialog.getOpenFileName( self, - _translate("MainWindow_reach", "Ouvrir un fichier"), + self._trad["open_file"], "", - _translate("MainWindow_reach", "Fichiers .ST (*.ST)") + - ";; " + - _translate("MainWindow_reach", "Fichiers .M (*.M)") + - ";; " + - _translate("MainWindow_reach", "Tous les fichiers (*)"), + ";; ".join(file_types), options=options ) @@ -158,20 +216,6 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.plot_kpc() self.plot_ac() - def messagebox_profile_editing(self): - msg_box = QtWidgets.QMessageBox() - msg_box.setIcon(QtWidgets.QMessageBox.Information) - msg_box.setWindowTitle(_translate("MainWindow_reach", - "Édition des profils sélectionnés")) - msg_box.setText(_translate("MainWindow_reach", - "Vous avez sélectionné plus de 5 profils." - " \nSeuls les 5 premiers seront édités.")) - msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) - - return_value = msg_box.exec() - # if return_value == QtWidgets.QMessageBox.Ok: - # print('OK clicked') - def edit_profile(self): self.tableView.model().blockSignals(True) @@ -184,20 +228,18 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): for row in rows: profile = self._reach.profile(row) - win = self.sub_win_filter_first( - "Profile", - contain = [self._reach.name, str(profile.kp)] - ) + if self.sub_window_exists( + ProfileWindow, + data=[None, None, profile] + ): + continue - if win is None: - win = ProfileWindow( - profile = profile, - parent = self, - ) - self._profile_window.append(win) - win.show() - else: - win.activateWindow() + win = ProfileWindow( + profile=profile, + parent=self, + ) + self._profile_window.append(win) + win.show() self.tableView.model().blockSignals(False) @@ -214,9 +256,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_xy = PlotXY( - canvas = self.ui.canvas_1, - data = self._reach, - toolbar = self.ui.toolbar_1 + canvas=self._canvas_xy, + data=self._reach, + toolbar=self._toolbar_xy ) self._plot_xy.draw() @@ -231,9 +273,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_kpc = PlotKPZ( - canvas = self.ui.canvas_2, - data = self._reach, - toolbar = self.ui.toolbar_2 + canvas=self._canvas_kpc, + data=self._reach, + toolbar=self._toolbar_kpc ) self._plot_kpc.draw() @@ -248,10 +290,10 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_ac = PlotAC( - canvas = self.ui.canvas_3, - data = self._reach, - toolbar = self.ui.toolbar_3, - plot_xy = self._plot_xy + canvas=self._canvas_ac, + data=self._reach, + toolbar=self._toolbar_ac, + plot_xy=self._plot_xy ) self._plot_ac.draw() @@ -303,7 +345,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): if len(self.tableView.selectedIndexes()) > 0: row = self.index_selected_row() - self.ui.verticalSlider.setValue(row) + self.find(QSlider, "verticalSlider").setValue(row) self.select_plot_xy(row) self.select_plot_kpc(row) self.select_plot_ac(row) @@ -314,41 +356,37 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) if self._tablemodel.rowCount() != 0: - self.ui.verticalSlider.setMaximum(self._tablemodel.rowCount() - 1) + slider = self.find(QSlider, "verticalSlider") + slider.setMaximum(self._tablemodel.rowCount() - 1) - slider_value = self.ui.verticalSlider.value() + slider_value = slider.value() kp = self._reach.profile(slider_value).kp - self.ui.vertical_slider_label.setText( - _translate("MainWindow_reach", "Kp : ") + - f"{kp}" + "\n" + - _translate("MainWindow_reach", - "Profil N° : ") + - f"{slider_value + 1}" - ) - self.select_plot_xy(slider_value) self.select_plot_kpc(slider_value) self.select_row_profile_slider(slider_value) + self.update_statusbar() self.tableView.model().blockSignals(False) def increment_value_slider(self): - if 0 <= self.ui.verticalSlider.value() < self._tablemodel.rowCount() - 1: - self.ui.verticalSlider.setValue(self.ui.verticalSlider.value() + 1) + slider = self.find(QSlider, "verticalSlider") + if 0 <= slider.value() < self._tablemodel.rowCount() - 1: + slider.setValue(slider.value() + 1) def decrement_value_slider(self): - if 0 < self.ui.verticalSlider.value() < self._tablemodel.rowCount(): - self.ui.verticalSlider.setValue(self.ui.verticalSlider.value() - 1) + slider = self.find(QSlider, "verticalSlider") + if 0 < slider.value() < self._tablemodel.rowCount(): + slider.setValue(slider.value() - 1) - def insert_row(self): + def add(self): if len(self.tableView.selectedIndexes()) == 0: - self._tablemodel.insert_row(self._tablemodel.rowCount()) + self._tablemodel.add(self._tablemodel.rowCount()) else: row = self.index_selected_row() - self._tablemodel.insert_row(row + 1) + self._tablemodel.add(row + 1) - def delete_rows(self): + def delete(self): rows = sorted( list( set( @@ -358,7 +396,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): ) if len(rows) > 0: - self._tablemodel.remove_rows(rows) + self._tablemodel.delete(rows) self.update_plot_xy() self.select_current_profile() @@ -373,24 +411,24 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): .row() def sort_ascending(self): - self._tablemodel.sort_profiles(True) + self._tablemodel.sort_profiles(False) self.select_current_profile() self.changed_slider_value() def sort_descending(self): - self._tablemodel.sort_profiles(False) + self._tablemodel.sort_profiles(True) self.select_current_profile() self.changed_slider_value() - def move_row_up(self): + def move_up(self): row = self.index_selected_row() - self._tablemodel.move_row_up(row) + self._tablemodel.move_up(row) self.select_current_profile() - def move_row_down(self): + def move_down(self): row = self.index_selected_row() - self._tablemodel.move_row_down(row) + self._tablemodel.move_down(row) self.select_current_profile() def duplicate(self): @@ -411,13 +449,13 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self._tablemodel.duplicate(rows, profiles) self.select_current_profile() - def copy(self): + def _copy(self): rows = self.tableView\ .selectionModel()\ .selectedRows() table = [] - table.append(["name", "kp"]) + # table.append(["name", "kp"]) for row in rows: profile = self._reach.profile(row.row()) @@ -427,47 +465,49 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: return - if len(header) != 0: - header.append("reach") + # if len(header) != 0: + # header.append("reach") + # header.append("status") + for row in data: row.append(self._reach) + row.append(self._study.river._status) row = self.index_selected_row() - self._tablemodel.paste(row, header, data) + # self._tablemodel.paste(row, header, data) + self._tablemodel.paste(row, [], data) self.select_current_profile() - def undo(self): + def _undo(self): self._tablemodel.undo() self.select_current_profile() self.update_plot_xy() self.update_plot_kpc() - def redo(self): + def _redo(self): self._tablemodel.redo() self.select_current_profile() self.update_plot_xy() self.update_plot_kpc() - def handleSave(self): + def export_to_file(self): options = QFileDialog.Options() DEFAULT_DIRECTORY = '/home/' - settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) - current_dir = settings.value('current_directory', DEFAULT_DIRECTORY, type=str) + settings = QSettings(QSettings.IniFormat, + QSettings.UserScope, 'MyOrg', ) + current_dir = settings.value( + 'current_directory', DEFAULT_DIRECTORY, type=str) options |= QFileDialog.DontUseNativeDialog filename, filters = QFileDialog.getSaveFileName( self, - filter=_translate("MainWindow_reach", - "Files .ST(*.ST or *.st)") - + ";; " + - _translate("MainWindow_reach", "All files " - "(*)"), + filter=self._trad["file_st"] + ";; " + self._trad["file_all"], options=options ) @@ -475,20 +515,3 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): if filename != '': self._tablemodel.export_reach(filename) - - def handleOpen(self): - filename, filterName = QFileDialog.getOpenFileName(self) - - if filename != '': - with open(filename, 'r') as f: - reader = csv.reader(f, delimiter='\t') - header = next(reader) - buf = [] - for row in reader: - row[0] = QCheckBox("-") - buf.append(row) - - self._tablemodel = None - self._tablemodel = TableEditableModel(buf) - self.tableView.setModel(self._tablemodel) - filename = '' diff --git a/src/View/Geometry/mainwindow_ui_reach.py b/src/View/Geometry/mainwindow_ui_reach.py deleted file mode 100644 index ff0797ab..00000000 --- a/src/View/Geometry/mainwindow_ui_reach.py +++ /dev/null @@ -1,357 +0,0 @@ -# mainwindow_ui_reach.py -- Pamhyr -# Copyright (C) 2023 INRAE -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# -*- coding: utf-8 -*- - -import sys -import os.path - -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import QSize, Qt -from PyQt5.QtWidgets import ( - QAbstractItemView, QHeaderView, QToolBar, -) - -from View.Plot import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar - - -file_path = os.path.abspath(os.path.dirname(__file__)) -icon_path = f"{os.path.dirname(os.path.dirname(file_path))}/View/ui/ressources/" - -class Ui_MainWindow(object): - def setupUi(self, MainWindow_reach): - MainWindow_reach.setObjectName("MainWindow_reach") - MainWindow_reach.resize(1400, 800) - self.centralwidget = QtWidgets.QWidget(MainWindow_reach) - self.centralwidget.setObjectName("centralwidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.splitter_3 = QtWidgets.QSplitter(self.centralwidget) - self.splitter_3.setOrientation(Qt.Horizontal) - self.splitter_3.setObjectName("splitter_3") - self.widget = QtWidgets.QWidget(self.splitter_3) - self.widget.setObjectName("widget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.widget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - - # self.label_section_order = QtWidgets.QLabel(self.widget) - # self.label_section_order.setLayoutDirection(Qt.LeftToRight) - # self.label_section_order.setObjectName("label_section_order") - - # self.verticalLayout.addWidget(self.label_section_order) - - self.toolBar = QToolBar(MainWindow_reach) - self.toolBar.setEnabled(True) - MainWindow_reach.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) - - btn_size = QSize(40, 28) - - self.btn_open = QtWidgets.QAction(self.widget) - self.btn_open.setObjectName("btn_open") - icon_btn_open = QtGui.QIcon() - icon_btn_open.addPixmap( - QtGui.QPixmap(f"{icon_path}open.png")) - self.btn_open.setIcon(icon_btn_open) - self.toolBar.addAction(self.btn_open) - - self.btn_add = QtWidgets.QAction(self.widget) - self.btn_add.setObjectName("btn_add") - icon_btn_add = QtGui.QIcon() - icon_btn_add.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk_add.png")) - self.btn_add.setIcon(icon_btn_add) - self.toolBar.addAction(self.btn_add) - - self.btn_delete = QtWidgets.QAction(self.widget) - self.btn_delete.setObjectName("btn_delete") - icon_btn_delete = QtGui.QIcon() - icon_btn_delete.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-remove.png")) - self.btn_delete.setIcon(icon_btn_delete) - self.toolBar.addAction(self.btn_delete) - - self.btn_edit = QtWidgets.QAction(self.widget) - self.btn_edit.setObjectName("btn_edit") - icon_btn_edit = QtGui.QIcon() - icon_btn_edit.addPixmap( - QtGui.QPixmap(f"{icon_path}gnome-stock-edit.png")) - self.btn_edit.setIcon(icon_btn_edit) - self.toolBar.addAction(self.btn_edit) - - # self.btn_copy_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_copy_selected_profile.setObjectName("btn_copy_selected_profile") - # icon_btn_copy_selected_profile = QtGui.QIcon() - # icon_btn_copy_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-copy.png")) - # self.btn_copy_selected_profile.setIcon(icon_btn_copy_selected_profile) - # self.toolBar.addAction(self.btn_copy_selected_profile) - - # self.btn_paste_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_paste_selected_profile.setObjectName("btn_paste_selected_profile") - # icon_btn_paste_selected_profile = QtGui.QIcon() - # icon_btn_paste_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-paste.png")) - # self.btn_paste_selected_profile.setIcon(icon_btn_paste_selected_profile) - # self.toolBar.addAction(self.btn_paste_selected_profile) - - # self.btn_duplicate_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_duplicate_selected_profile.setObjectName("btn_duplicate_selected_profile") - # icon_btn_duplicate_selected_profile = QtGui.QIcon() - # icon_btn_duplicate_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-dnd-multiple.png")) - # self.btn_duplicate_selected_profile.setIcon(icon_btn_duplicate_selected_profile) - # self.toolBar.addAction(self.btn_duplicate_selected_profile) - - self.btn_sort_asc = QtWidgets.QAction(self.widget) - self.btn_sort_asc.setObjectName("btn_sort_asc") - icon_btn_sort_asc = QtGui.QIcon() - icon_btn_sort_asc.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc.setIcon(icon_btn_sort_asc) - self.toolBar.addAction(self.btn_sort_asc) - - self.btn_sort_desc = QtWidgets.QAction(self.widget) - self.btn_sort_desc.setObjectName("btn_sort_desc") - icon_btn_sort_desc = QtGui.QIcon() - icon_btn_sort_desc.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc.setIcon(icon_btn_sort_desc) - self.toolBar.addAction(self.btn_sort_desc) - - self.btn_move_up = QtWidgets.QAction(self.widget) - self.btn_move_up.setObjectName("btn_move_up") - icon_btn_move_up = QtGui.QIcon() - icon_btn_move_up.addPixmap( - QtGui.QPixmap(f"{icon_path}up.png")) - self.btn_move_up.setIcon(icon_btn_move_up) - self.toolBar.addAction(self.btn_move_up) - - self.btn_move_down = QtWidgets.QAction(self.widget) - self.btn_move_down.setObjectName("btn_move_down") - icon_btn_move_down = QtGui.QIcon() - icon_btn_move_down.addPixmap( - QtGui.QPixmap(f"{icon_path}down.png")) - self.btn_move_down.setIcon(icon_btn_move_down) - self.toolBar.addAction(self.btn_move_down) - - # self.btn_end_editing = QtWidgets.QAction(self.widget) - # self.btn_end_editing.setObjectName("btn_end_editing") - # icon_btn_save = QtGui.QIcon() - # icon_btn_save.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-apply.png")) - # self.btn_end_editing.setIcon(icon_btn_save) - # self.toolBar.addAction(self.btn_end_editing) - - self.verticalLayout.addLayout(self.horizontalLayout) - self.tableView = QtWidgets.QTableView(self.widget) - self.tableView.setObjectName("tableView") - self.verticalLayout.addWidget(self.tableView) - self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) - self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.tableView.setAlternatingRowColors(True) - self.tableView.verticalHeader().hide() - self.tableView.resizeColumnsToContents() - self.tableView.resizeRowsToContents() - - self.widget1 = QtWidgets.QWidget(self.splitter_3) - self.widget1.setObjectName("widget1") - self.gridLayout = QtWidgets.QGridLayout(self.widget1) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.splitter_2 = QtWidgets.QSplitter(self.widget1) - self.splitter_2.setOrientation(Qt.Vertical) - self.splitter_2.setObjectName("splitter_2") - self.splitter = QtWidgets.QSplitter(self.splitter_2) - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.verticalLayoutWidget_3 = QtWidgets.QWidget(self.splitter) - self.verticalLayoutWidget_3.setObjectName("verticalLayoutWidget_3") - self.verticalLayout_canvas_1 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget_3) - self.verticalLayout_canvas_1.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_1.setObjectName("verticalLayout_canvas_1") - - self.canvas_1 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_1.setObjectName("canvas_1") - - self.toolbar_1 = PamhyrPlotToolbar( - self.canvas_1, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_1.addWidget(self.toolbar_1) - self.verticalLayout_canvas_1.addWidget(self.canvas_1) - - self.verticalLayoutWidget_2 = QtWidgets.QWidget(self.splitter) - self.verticalLayoutWidget_2.setObjectName("verticalLayoutWidget_2") - self.verticalLayout_canvas_2 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget_2) - self.verticalLayout_canvas_2.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_2.setObjectName("verticalLayout_canvas_2") - - self.canvas_2 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_2.setObjectName("canvas_2") - - self.toolbar_2 = PamhyrPlotToolbar( - self.canvas_2, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_2.addWidget(self.toolbar_2) - self.verticalLayout_canvas_2.addWidget(self.canvas_2) - self.verticalLayoutWidget = QtWidgets.QWidget(self.splitter_2) - self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") - self.verticalLayout_canvas_3 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) - self.verticalLayout_canvas_3.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_3.setObjectName("verticalLayout_canvas_3") - - self.canvas_3 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_3.setObjectName("canvas_3") - - self.toolbar_3 = PamhyrPlotToolbar( - self.canvas_3, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_3.addWidget(self.toolbar_3) - self.verticalLayout_canvas_3.addWidget(self.canvas_3) - - self.gridLayout.addWidget(self.splitter_2, 0, 0, 3, 3) - self.verticalSlider = QtWidgets.QSlider(self.widget1) - self.verticalSlider.setMinimumSize(QtCore.QSize(30, 0)) - self.verticalSlider.setOrientation(Qt.Vertical) - self.verticalSlider.setInvertedAppearance(True) - self.verticalSlider.setInvertedControls(False) - self.verticalSlider.setObjectName("verticalSlider") - self.gridLayout.addWidget(self.verticalSlider, 0, 3, 1, 1) - - self.btn_slider_up = QtWidgets.QPushButton(self.widget1) - self.btn_slider_up.setMaximumSize(btn_size) - self.btn_slider_up.setText("") - self.btn_slider_up.setObjectName("btn_slider_up") - icon_btn_slider_up = QtGui.QIcon() - icon_btn_slider_up.addPixmap( - QtGui.QPixmap(f"{icon_path}go-up2.png")) - self.btn_slider_up.setIcon(icon_btn_slider_up) - #self.btn_slider_up.setIconSize(btn_size) - self.gridLayout.addWidget(self.btn_slider_up, 1, 3, 1, 1) - - self.btn_slider_down = QtWidgets.QPushButton(self.widget1) - self.btn_slider_down.setMaximumSize(btn_size) - self.btn_slider_down.setText("") - self.btn_slider_down.setObjectName("btn_slider_down") - icon_btn_slider_down = QtGui.QIcon() - icon_btn_slider_down.addPixmap( - QtGui.QPixmap(f"{icon_path}go-down1.png")) - self.btn_slider_down.setIcon(icon_btn_slider_down) - #self.btn_slider_down.setIconSize(btn_size) - self.gridLayout.addWidget(self.btn_slider_down, 2, 3, 1, 1) - - self.vertical_slider_label = QtWidgets.QLabel(self.widget1) - self.vertical_slider_label.setObjectName("vertical_slider_label") - self.gridLayout.addWidget(self.vertical_slider_label, 3, 2, 2, 1) - self.vertical_slider_label.setAlignment(Qt.AlignRight) - - # self.btn_slider_go_back = QtWidgets.QPushButton(self.widget1) - # self.btn_slider_go_back.setMaximumSize(btn_size) - # self.btn_slider_go_back.setText("") - # self.btn_slider_go_back.setObjectName("btn_slider_go_back") - # icon_btn_slider_go_back = QtGui.QIcon() - # icon_btn_slider_go_back.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-go-back.png")) - # self.btn_slider_go_back.setIcon(icon_btn_slider_go_back) - # #self.btn_slider_go_back.setIconSize(btn_size) - # self.gridLayout.addWidget(self.btn_slider_go_back, 5, 0, 1, 1) - # self.btn_slider_go_back.setEnabled(False) - - # self.btn_slider_go_forward = QtWidgets.QPushButton(self.widget1) - # self.btn_slider_go_forward.setMaximumSize(btn_size) - # self.btn_slider_go_forward.setText("") - # self.btn_slider_go_forward.setObjectName("btn_slider_go_forward") - # icon_btn_slider_go_forward = QtGui.QIcon() - # icon_btn_slider_go_forward.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-go-forward.png")) - # self.btn_slider_go_forward.setIcon(icon_btn_slider_go_forward) - # #self.btn_slider_go_forward.setIconSize(btn_size) - # self.gridLayout.addWidget(self.btn_slider_go_forward, 5, 1, 1, 1) - # self.btn_slider_go_forward.setEnabled(False) - - # self.horizontalSlider = QtWidgets.QSlider(self.widget1) - # self.horizontalSlider.setMinimumSize(QtCore.QSize(0, 30)) - # self.horizontalSlider.setOrientation(Qt.Horizontal) - # self.horizontalSlider.setObjectName("horizontalSlider") - # self.gridLayout.addWidget(self.horizontalSlider, 5, 2, 1, 1) - # self.horizontalSlider.setEnabled(False) - - self.horizontalLayout_2.addWidget(self.splitter_3) - MainWindow_reach.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow_reach) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1123, 20)) - self.menubar.setObjectName("menubar") - MainWindow_reach.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow_reach) - self.statusbar.setObjectName("statusbar") - MainWindow_reach.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindow_reach) - QtCore.QMetaObject.connectSlotsByName(MainWindow_reach) - - def retranslateUi(self, MainWindow_reach): - _translate = QtCore.QCoreApplication.translate - self.name_reach = "" - self.mainwindow_title = _translate("MainWindow_reach", "Geometry reach edit") - MainWindow_reach.setWindowTitle(self.mainwindow_title + f"{self.name_reach}") - # self.label_section_order.setText(f"{self.text_section_order}") - self.vertical_slider_value = "" - self.vertical_slider_label.setText(_translate("MainWindow_reach", "Kp = ") + f"{self.vertical_slider_value}") - self.btn_open.setToolTip(_translate("MainWindow_reach", " Import geometry")) - self.btn_add.setToolTip(_translate("MainWindow_reach", " New profile")) - self.btn_delete.setToolTip(_translate("MainWindow_reach", " Delete selected profile")) - self.btn_edit.setToolTip(_translate("MainWindow_reach", " Editer selected profile")) - - self.btn_sort_asc.setToolTip(_translate("MainWindow_reach", " Trier les profils par ordre croissant des Pk")) - self.btn_sort_desc.setToolTip(_translate("MainWindow_reach", " Trier les profils par ordre décroissant des Pk")) - self.btn_move_up.setToolTip(_translate("MainWindow_reach", " Changer l'ordre des profils (en décalant le " - "profil sélectionné vers le haut)")) - self.btn_move_down.setToolTip(_translate("MainWindow_reach", " Changer l'ordre des profils (en décalant le " - "profil sélectionné vers le bas)")) - # self.btn_end_editing.setToolTip(_translate("MainWindow_reach", " Terminer l'édition")) - self.tableView_header = [_translate("MainWindow_reach", "Name"), - _translate("MainWindow_reach", "Kp (m)"), - _translate("MainWindow_reach", "Type")] - - self.toolbar_1._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+Z")) - self.toolbar_1._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+Z)")) - - self.toolbar_2._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+E")) - self.toolbar_2._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+E)")) - - self.toolbar_3._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+R")) - self.toolbar_3._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+R)")) - - self.toolbar_1._actions["non_isometric_view"].setShortcut("Alt+S") - self.toolbar_1._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+S)")) - - self.toolbar_2._actions["non_isometric_view"].setShortcut("Alt+D") - self.toolbar_2._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+D)")) - - self.toolbar_3._actions["non_isometric_view"].setShortcut("Alt+F") - self.toolbar_3._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+F)")) diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py new file mode 100644 index 00000000..ed81c131 --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py @@ -0,0 +1,275 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QMessageBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.HydraulicStructures.BasicHydraulicStructures.UndoCommand import ( + SetNameCommand, SetTypeCommand, + SetEnabledCommand, AddCommand, DelCommand, + SetValueCommand, +) +from Model.HydraulicStructures.Basic.Types import BHS_types + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + lst = list( + map( + lambda k: self._long_types[k], + BHS_types.keys() + ) + ) + self.editor.addItems(lst) + + self.editor.setCurrentText(index.data(Qt.DisplayRole)) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(PamhyrTableModel): + def __init__(self, trad=None, **kwargs): + self._trad = trad + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + super(TableModel, self).__init__(trad=trad, **kwargs) + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._data.basic_structure(row).name + elif self._headers[column] == "type": + return self._long_types[self._data.basic_structure(row).type] + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._data, row, value + ) + ) + elif self._headers[column] == "type": + if self._question_set_type(): + key = next( + k for k, v in self._long_types.items() + if v == value + ) + + self._undo.push( + SetTypeCommand( + self._data, row, BHS_types[key] + ) + ) + except Exception as e: + logger.error(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def _question_set_type(self): + question = QMessageBox(self._parent) + + question.setWindowTitle(self._trad['msg_type_change_title']) + question.setText(self._trad['msg_type_change_text']) + question.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) + question.setIcon(QMessageBox.Question) + + res = question.exec() + return res == QMessageBox.Ok + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._data, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._data, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def enabled(self, row, enabled, parent=QModelIndex()): + self._undo.push( + SetEnabledCommand( + self._lst, row, enabled + ) + ) + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() + + +class ParametersTableModel(PamhyrTableModel): + def __init__(self, trad=None, **kwargs): + self._trad = trad + self._long_types = {} + + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + self._hs_index = None + + super(ParametersTableModel, self).__init__(trad=trad, **kwargs) + + def rowCount(self, parent): + if self._hs_index is None: + return 0 + + return len( + self._data.basic_structure(self._hs_index) + ) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + if self._hs_index is None: + return QVariant() + + row = index.row() + column = index.column() + + hs = self._data.basic_structure(self._hs_index) + + if self._headers[column] == "name": + return hs.parameters[row].name + elif self._headers[column] == "value": + return str(hs.parameters[row].value) + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + if self._hs_index is None: + return QVariant() + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "value": + self._undo.push( + SetValueCommand( + self._data.basic_structure(self._hs_index), + row, value + ) + ) + except Exception as e: + logger.error(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def update_hs_index(self, index): + self._hs_index = index + self.layoutChanged.emit() diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py new file mode 100644 index 00000000..6681219e --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py @@ -0,0 +1,85 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class BasicHydraulicStructuresTranslate(PamhyrTranslate): + def __init__(self): + super(BasicHydraulicStructuresTranslate, self).__init__() + + self._dict['msg_type_change_title'] = _translate( + "BasicHydraulicStructures", + "Change hydraulic structure type" + ) + + self._dict['msg_type_change_text'] = _translate( + "BasicHydraulicStructures", + "Do you want to change the hydraulic structure type and reset \ +hydraulic structure values?" + ) + + self._sub_dict["long_types"] = { + "ND": _translate( + "BasicHydraulicStructures", "Not defined" + ), + "S1": _translate( + "BasicHydraulicStructures", "Seuil déversoir" + ), + "S2": _translate( + "BasicHydraulicStructures", "Seuil trapezoidal" + ), + "S3": _translate( + "BasicHydraulicStructures", "Seuil triangulaire" + ), + "OR": _translate( + "BasicHydraulicStructures", "Orifice rectangulaire" + ), + "OC": _translate( + "BasicHydraulicStructures", "Orifice circulaire" + ), + "OV": _translate( + "BasicHydraulicStructures", "Orifice voute" + ), + "V1": _translate( + "BasicHydraulicStructures", "Vanne rectangulaire" + ), + "V2": _translate( + "BasicHydraulicStructures", "Vanne rectangulaire simplifiée" + ), + "BO": _translate( + "BasicHydraulicStructures", "Perte de charge à la Borda" + ), + "UD": _translate( + "BasicHydraulicStructures", "User defined" + ), + } + + self._sub_dict["table_headers"] = { + "name": _translate("BasicHydraulicStructures", "Name"), + "type": _translate("BasicHydraulicStructures", "Type"), + } + + self._sub_dict["table_headers_parameters"] = { + "name": _translate("BasicHydraulicStructures", "Name"), + "value": _translate("BasicHydraulicStructures", "Value"), + } diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py b/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py new file mode 100644 index 00000000..124a446c --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py @@ -0,0 +1,153 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + + +class SetNameCommand(QUndoCommand): + def __init__(self, hs, index, new_value): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._old = self._hs.basic_structure(self._index).name + self._new = str(new_value) + + def undo(self): + self._hs.basic_structure(self._index).name = self._old + + def redo(self): + self._hs.basic_structure(self._index).name = self._new + + +class SetTypeCommand(QUndoCommand): + def __init__(self, hs, index, new_type): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._type = new_type + self._old = self._hs.basic_structure(self._index) + self._new = self._hs.basic_structure(self._index)\ + .convert(self._type) + + def undo(self): + self._hs.delete_i([self._index]) + self._hs.insert(self._index, self._old) + + def redo(self): + self._hs.delete_i([self._index]) + self._hs.insert(self._index, self._new) + + +class SetEnabledCommand(QUndoCommand): + def __init__(self, hs, index, enabled): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._old = not enabled + self._new = enabled + + def undo(self): + self._hs.basic_structure(self._index).enabled = self._old + + def redo(self): + self._hs.basic_structure(self._index).enabled = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, hs, index): + QUndoCommand.__init__(self) + + self._hs = hs + + self._index = index + self._new = None + + def undo(self): + self._hs.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._hs.add(self._index) + else: + self._hs.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, hs, rows): + QUndoCommand.__init__(self) + + self._hs = hs + + self._rows = rows + + self._bhs = [] + for row in rows: + self._bhs.append((row, self._hs.basic_structure(row))) + + def undo(self): + for row, el in self._bhs: + self._hs.insert(row, el) + + def redo(self): + self._hs.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, hs, row, h_s): + QUndoCommand.__init__(self) + + self._hs = hs + + self._row = row + self._bhs = deepcopy(h_s) + self._bhs.reverse() + + def undo(self): + self._hs.delete_i(range(self._row, self._row + len(self._bhs))) + + def redo(self): + for r in self._bhs: + self._hs.insert(self._row, r) + +#################################### +# Basic hydraulic structure values # +#################################### + + +class SetValueCommand(QUndoCommand): + def __init__(self, bhs, index, value): + QUndoCommand.__init__(self) + + self._bhs = bhs + self._index = index + self._old = self._bhs.parameters[self._index].value + self._new = self._bhs.parameters[self._index].type(value) + + def undo(self): + self._bhs.parameters[self._index].value = self._old + + def redo(self): + self._bhs.parameters[self._index].value = self._new diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py new file mode 100644 index 00000000..ca4b2680 --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py @@ -0,0 +1,265 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, QItemSelectionModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.HydraulicStructures.PlotAC import PlotAC + +from View.HydraulicStructures.BasicHydraulicStructures.Table import ( + ComboBoxDelegate, TableModel, ParametersTableModel, +) + +from View.Network.GraphWidget import GraphWidget +from View.HydraulicStructures.BasicHydraulicStructures.Translate import ( + BasicHydraulicStructuresTranslate +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class BasicHydraulicStructuresWindow(PamhyrWindow): + _pamhyr_ui = "BasicHydraulicStructures" + _pamhyr_name = "Basic Hydraulic Structures" + + def __init__(self, data=None, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name + + super(BasicHydraulicStructuresWindow, self).__init__( + title=name, + study=study, + config=config, + trad=BasicHydraulicStructuresTranslate(), + parent=parent + ) + + self._hash_data.append(data) + + self._hs = data + + self.setup_table() + self.setup_checkbox() + self.setup_plot() + self.setup_connections() + + self.update() + + def setup_table(self): + self.setup_table_bhs() + self.setup_table_bhs_parameters() + + def setup_table_bhs(self): + self._table = None + + self._delegate_type = ComboBoxDelegate( + trad=self._trad, + parent=self + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "type"], + delegates={ + "type": self._delegate_type, + }, + trad=self._trad, + data=self._hs, + undo=self._undo_stack, + parent=self, + ) + + selectionModel = table.selectionModel() + index = table.model().index(0, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def setup_table_bhs_parameters(self): + self._table_parameters = None + + table = self.find(QTableView, f"tableView_2") + self._table_parameters = ParametersTableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers_parameters"), + editable_headers=["value"], + delegates={}, + trad=self._trad, + data=self._hs, + undo=self._undo_stack, + parent=self, + ) + + def setup_checkbox(self): + self._checkbox = self.find(QCheckBox, f"checkBox") + self._set_checkbox_state() + + def setup_plot(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) + self.plot_layout.addWidget(self.canvas) + + reach = self._hs.input_reach + profile_kp = self._hs.input_kp + if profile_kp is not None: + profiles = reach.reach.get_profiles_from_kp(float(profile_kp)) + else: + profiles = None + if profiles is not None: + profile = profiles[0] + else: + profile = None + + self.plot_ac = PlotAC( + canvas=self.canvas, + river=self._study.river, + reach=self._hs.input_reach, + profile=profile, + toolbar=self.toolbar + ) + self.plot_ac.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self._checkbox.clicked.connect(self._set_basic_structure_state) + + table = self.find(QTableView, "tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self.update) + + self._table.dataChanged.connect(self.update) + self._table.layoutChanged.connect(self.update) + + def index_selected(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0] + else: + return None + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0].row() + else: + return None + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + + if len(self._hs) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + + if len(rows) == 0: + return + + self._table.delete(rows) + + def _copy(self): + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + self._table.undo() + + def _redo(self): + self._table.redo() + + def _set_checkbox_state(self): + row = self.index_selected_row() + + if row is None: + self._checkbox.setEnabled(False) + self._checkbox.setChecked(True) + else: + self._checkbox.setEnabled(True) + self._checkbox.setChecked(self._hs.basic_structure(row).enabled) + + def _set_basic_structure_state(self): + row = self.index_selected_row() + + if row is not None: + self._table.enabled( + row, + self._checkbox.isChecked() + ) + + def update(self): + self._set_checkbox_state() + self._update_parameters_table() + + def _update_parameters_table(self): + row = self.index_selected_row() + self._table_parameters.update_hs_index(row) diff --git a/src/View/HydraulicStructures/PlotAC.py b/src/View/HydraulicStructures/PlotAC.py new file mode 100644 index 00000000..4d171968 --- /dev/null +++ b/src/View/HydraulicStructures/PlotAC.py @@ -0,0 +1,133 @@ +# PlotAC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from tools import timer +from View.Tools.PamhyrPlot import PamhyrPlot +from matplotlib import pyplot as plt + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + + +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + river=None, reach=None, profile=None, + parent=None): + super(PlotAC, self).__init__( + canvas=canvas, + trad=trad, + data=river, + toolbar=toolbar, + parent=parent + ) + + self._current_reach = reach + self._current_profile = profile + + @property + def river(self): + return self.data + + @river.setter + def river(self, river): + self.data = river + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + self.line_kp = None + return + + if self._current_reach is None: + self.line_kp = None + return + + reach = self._current_reach + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='black', fontsize=11 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Elevation (m)"), + color='black', fontsize=11 + ) + + if self._current_profile is None: + self.line_kp = None + else: + profile = self._current_profile + x = profile.get_station() + z = profile.z() + + self.line_kp, = self.canvas.axes.plot( + x, z, + linestyle="solid", + lw=1.8, + color='grey', + ) + + self.canvas.axes.relim() + self.canvas.axes.autoscale_view() + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach): + self._current_reach = reach + self.update() + + def set_profile(self, profile): + self._current_profile = profile + self.update() + + def update(self): + if self.line_kp is None: + self.draw() + return + + if self._current_reach is None or self._current_profile is None: + self.clear() + return + + profile = self._current_profile + x = profile.get_station() + z = profile.z() + + self.line_kp.set_data(x, z) + + self.canvas.axes.relim() + self.canvas.axes.autoscale_view() + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + + def clear(self): + if self.line_kp is not None: + self.line_kp.set_data([], []) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/HydraulicStructures/PlotKPC.py b/src/View/HydraulicStructures/PlotKPC.py new file mode 100644 index 00000000..f72cf7fc --- /dev/null +++ b/src/View/HydraulicStructures/PlotKPC.py @@ -0,0 +1,161 @@ +# PlotKPC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from tools import timer +from View.Tools.PamhyrPlot import PamhyrPlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +from matplotlib.collections import LineCollection + +_translate = QCoreApplication.translate + + +class PlotKPC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + river=None, reach=None, profile=None, + parent=None): + super(PlotKPC, self).__init__( + canvas=canvas, + trad=trad, + data=river, + toolbar=toolbar, + parent=parent + ) + + self._current_reach = reach + self._current_profile = profile + + @property + def river(self): + return self.data + + @river.setter + def river(self, river): + self.data = river + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + self.profile = None + self.line_kp_zmin_zmax = None + self.line_kp_zmin = None + return + + if self._current_reach is None: + self.profile = None + self.line_kp_zmin_zmax = None + self.line_kp_zmin = None + return + + reach = self._current_reach + + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Elevation (m)"), + color='black', fontsize=11 + ) + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "KP (m)"), + color='black', fontsize=11 + ) + + kp = reach.reach.get_kp() + z_min = reach.reach.get_z_min() + z_max = reach.reach.get_z_max() + + self.line_kp_zmin, = self.canvas.axes.plot( + kp, z_min, + color='grey', lw=1. + ) + + if len(kp) != 0: + self.line_kp_zmin_zmax = self.canvas.axes.vlines( + x=kp, + ymin=z_min, ymax=z_max, + color='b', + lw=1. + ) + + if self._current_profile is None: + self.profile = None + else: + self.profile, = self.canvas.axes.plot( + [self._current_profile.kp, self._current_profile.kp], + [self._current_profile.z_min(), self._current_profile.z_max()], + color='red', lw=1. + ) + + self.canvas.axes.relim() + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach): + self._current_reach = reach + self._current_profile = None + self.update() + + def set_profile(self, profile): + self._current_profile = profile + self.update_profil() + + def update(self): + self.draw() + + def update_profil(self): + reach = self._current_reach + kp = reach.reach.get_kp() + z_min = reach.reach.get_z_min() + z_max = reach.reach.get_z_max() + + if self.profile is None: + self.draw() + else: + self.profile.set_data( + [self._current_profile.kp, self._current_profile.kp], + [self._current_profile.z_min(), self._current_profile.z_max()], + ) + + self.canvas.axes.relim() + self.canvas.axes.autoscale_view() + self.canvas.figure.canvas.draw_idle() + + def clear(self): + if self.profile is not None: + self.profile.set_data([], []) + + if self.line_kp_zmin_zmax is not None: + self.line_kp_zmin_zmax.remove() + self.line_kp_zmin_zmax = None + + if self.line_kp_zmin is not None: + self.line_kp_zmin.set_data([], []) + + self.canvas.figure.canvas.draw_idle() + + def clear_profile(self): + if self.profile is not None: + self.profile.set_data([], []) + + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/HydraulicStructures/Table.py b/src/View/HydraulicStructures/Table.py new file mode 100644 index 00000000..210cf1e8 --- /dev/null +++ b/src/View/HydraulicStructures/Table.py @@ -0,0 +1,215 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.HydraulicStructures.UndoCommand import ( + SetNameCommand, SetReachCommand, SetKpCommand, + SetEnabledCommand, AddCommand, DelCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None, mode="reaches"): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + self._mode = mode + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + val = [] + if self._mode == "kp": + reach = self._data.hydraulic_structures\ + .get(index.row())\ + .input_reach + if reach is not None: + val = list( + map( + lambda kp: str(kp), reach.reach.get_kp() + ) + ) + else: + val = list( + map( + lambda n: n.name, self._data.edges() + ) + ) + + self.editor.addItems( + [_translate("Hydraulic structure", "Not associated")] + + val + ) + + self.editor.setCurrentText(str(index.data(Qt.DisplayRole))) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data._hydraulic_structures + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._lst.get(row).name + elif self._headers[column] == "reach": + n = self._lst.get(row).input_reach + if n is None: + return _translate("Hydraulic structure", "Not associated") + return n.name + elif self._headers[column] == "kp": + n = self._lst.get(row).input_kp + if n is None: + return _translate("Hydraulic structure", "Not associated") + return n + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + na = _translate("Hydraulic structure", "Not associated") + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._lst, row, value + ) + ) + elif self._headers[column] == "reach": + if value == na: + value = None + + self._undo.push( + SetReachCommand( + self._lst, row, self._data.edge(value) + ) + ) + elif self._headers[column] == "kp": + if value == na: + value = None + + self._undo.push( + SetKpCommand( + self._lst, row, value + ) + ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._lst, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._lst, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def enabled(self, row, enabled, parent=QModelIndex()): + self._undo.push( + SetEnabledCommand( + self._lst, row, enabled + ) + ) + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/HydraulicStructures/Translate.py b/src/View/HydraulicStructures/Translate.py new file mode 100644 index 00000000..1db3faec --- /dev/null +++ b/src/View/HydraulicStructures/Translate.py @@ -0,0 +1,34 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class HydraulicStructuresTranslate(PamhyrTranslate): + def __init__(self): + super(HydraulicStructuresTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("HydraulicStructures", "Name"), + "reach": _translate("HydraulicStructures", "Reach"), + "kp": _translate("HydraulicStructures", "Kp"), + } diff --git a/src/View/HydraulicStructures/UndoCommand.py b/src/View/HydraulicStructures/UndoCommand.py new file mode 100644 index 00000000..9c47ccf1 --- /dev/null +++ b/src/View/HydraulicStructures/UndoCommand.py @@ -0,0 +1,159 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +logger = logging.getLogger() + + +class SetNameCommand(QUndoCommand): + def __init__(self, h_s_lst, index, new_value): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).name + self._new = str(new_value) + + def undo(self): + self._h_s_lst.get(self._index).name = self._old + + def redo(self): + self._h_s_lst.get(self._index).name = self._new + + +class SetReachCommand(QUndoCommand): + def __init__(self, h_s_lst, index, reach): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).input_reach + self._new = reach + self._old_kp = self._h_s_lst.get(self._index).input_kp + self._new_kp = None + + def undo(self): + i = self._h_s_lst.get(self._index) + i.input_reach = self._old + i.input_kp = self._old_kp + + def redo(self): + i = self._h_s_lst.get(self._index) + i.input_reach = self._new + i.input_kp = self._new_kp + + +class SetKpCommand(QUndoCommand): + def __init__(self, h_s_lst, index, kp): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).input_kp + self._new = kp + + def undo(self): + self._h_s_lst.get(self._index).input_kp = self._old + + def redo(self): + self._h_s_lst.get(self._index).input_kp = self._new + + +class SetEnabledCommand(QUndoCommand): + def __init__(self, h_s_lst, index, enabled): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = not enabled + self._new = enabled + + def undo(self): + self._h_s_lst.get(self._index).enabled = self._old + + def redo(self): + self._h_s_lst.get(self._index).enabled = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, h_s_lst, index): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._index = index + self._new = None + + def undo(self): + self._h_s_lst.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._h_s_lst.new(self._h_s_lst, self._index) + else: + self._h_s_lst.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, h_s_lst, rows): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._rows = rows + + self._h_s = [] + for row in rows: + self._h_s.append((row, self._h_s_lst.get(row))) + self._h_s.sort() + + def undo(self): + for row, el in self._h_s: + self._h_s_lst.insert(row, el) + + def redo(self): + self._h_s_lst.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, h_s_lst, row, h_s): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._row = row + self._h_s = deepcopy(h_s) + self._h_s.reverse() + + def undo(self): + self._h_s_lst.delete_i( + self._tab, + range(self._row, self._row + len(self._h_s)) + ) + + def redo(self): + for r in self._h_s: + self._h_s_lst.insert(self._row, r) diff --git a/src/View/HydraulicStructures/Window.py b/src/View/HydraulicStructures/Window.py new file mode 100644 index 00000000..b0b80bf9 --- /dev/null +++ b/src/View/HydraulicStructures/Window.py @@ -0,0 +1,313 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, QItemSelectionModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.HydraulicStructures.PlotAC import PlotAC +from View.HydraulicStructures.PlotKPC import PlotKPC + +from View.HydraulicStructures.Table import ( + TableModel, ComboBoxDelegate +) + +from View.Network.GraphWidget import GraphWidget +from View.HydraulicStructures.Translate import HydraulicStructuresTranslate + +from View.HydraulicStructures.BasicHydraulicStructures.Window import ( + BasicHydraulicStructuresWindow +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class HydraulicStructuresWindow(PamhyrWindow): + _pamhyr_ui = "HydraulicStructures" + _pamhyr_name = "Hydraulic Structures" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name + + super(HydraulicStructuresWindow, self).__init__( + title=name, + study=study, + config=config, + trad=HydraulicStructuresTranslate(), + parent=parent + ) + + self._hs_lst = self._study.river._hydraulic_structures + + self.setup_table() + self.setup_checkbox() + self.setup_plots() + self.setup_connections() + + self.update() + + def setup_table(self): + self._table = None + + self._delegate_reach = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self, + mode="reaches" + ) + self._delegate_kp = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self, + mode="kp" + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "reach", "kp"], + delegates={ + "reach": self._delegate_reach, + "kp": self._delegate_kp, + }, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + ) + + selectionModel = table.selectionModel() + index = table.model().index(0, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def setup_checkbox(self): + self._checkbox = self.find(QCheckBox, f"checkBox") + self._set_checkbox_state() + + def setup_plots(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) + self.plot_layout.addWidget(self.canvas) + + self.plot_kpc = PlotKPC( + canvas=self.canvas, + river=self._study.river, + reach=None, + profile=None, + toolbar=self.toolbar + ) + self.plot_kpc.draw() + + self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_2.setObjectName("canvas_2") + self.toolbar_2 = PamhyrPlotToolbar( + self.canvas_2, self + ) + self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout_2.addWidget(self.toolbar_2) + self.plot_layout_2.addWidget(self.canvas_2) + + self.plot_ac = PlotAC( + canvas=self.canvas_2, + river=self._study.river, + reach=None, + profile=None, + toolbar=self.toolbar_2 + ) + self.plot_ac.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self.find(QAction, "action_edit").triggered.connect(self.edit) + self._checkbox.clicked.connect(self._set_structure_state) + + table = self.find(QTableView, "tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self.update) + + self._table.dataChanged.connect(self.update) + self._table.layoutChanged.connect(self.update) + + def index_selected(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0] + else: + return None + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0].row() + else: + return None + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._hs_lst) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + + def _copy(self): + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + self._table.undo() + + def _redo(self): + self._table.redo() + + def edit(self): + rows = self.index_selected_rows() + for row in rows: + data = self._hs_lst.get(row) + print(row) + print(data) + if self.sub_window_exists( + BasicHydraulicStructuresWindow, + data=[self._study, None, data] + ): + continue + + win = BasicHydraulicStructuresWindow( + data=data, + study=self._study, + parent=self + ) + win.show() + + def _set_checkbox_state(self): + row = self.index_selected_row() + if row is None: + self._checkbox.setEnabled(False) + self._checkbox.setChecked(True) + else: + self._checkbox.setEnabled(True) + self._checkbox.setChecked(self._hs_lst.get(row).enabled) + + def _set_structure_state(self): + row = self.index_selected_row() + if row is not None: + self._table.enabled( + row, + self._checkbox.isChecked() + ) + + def update(self): + self._set_checkbox_state() + self._update_clear_plot() + + def _update_clear_plot(self): + rows = self.index_selected_rows() + + if len(rows) == 0 or len(self._hs_lst) == 0: + self._update_clear_all() + return + + reach = self._hs_lst.get(rows[0]).input_reach + if reach is not None: + self.plot_kpc.set_reach(reach) + self.plot_ac.set_reach(reach) + + profile_kp = self._hs_lst.get(rows[0]).input_kp + if profile_kp is not None: + profiles = reach.reach\ + .get_profiles_from_kp( + float(profile_kp) + ) + + if profiles is not None: + profile = profiles[0] + + self.plot_kpc.set_profile(profile) + self.plot_ac.set_profile(profile) + else: + self._update_clear_profile() + else: + self._update_clear_profile() + else: + self._update_clear_all() + + def _update_clear_all(self): + self.plot_kpc.clear() + self.plot_ac.clear() + + def _update_clear_profile(self): + self.plot_ac.clear() + self.plot_kpc.clear_profile() diff --git a/src/View/InitialConditions/DialogDischarge.py b/src/View/InitialConditions/DialogDischarge.py index f8e7e32a..df0461bd 100644 --- a/src/View/InitialConditions/DialogDischarge.py +++ b/src/View/InitialConditions/DialogDischarge.py @@ -16,8 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -32,10 +31,16 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) -class DischargeDialog(ASubWindow, ListedSubWindow): + +class DischargeDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Discharge" + _pamhyr_name = "Discharge" + def __init__(self, title="Discharge", parent=None): super(DischargeDialog, self).__init__( - name=title, ui="InitialConditions_Dialog_Generator_Discharge", parent=parent + title=self._pamhyr_name, + options=[], + parent=parent ) self.value = None diff --git a/src/View/InitialConditions/DialogHeight.py b/src/View/InitialConditions/DialogHeight.py index 18ad67c8..0cd62c88 100644 --- a/src/View/InitialConditions/DialogHeight.py +++ b/src/View/InitialConditions/DialogHeight.py @@ -16,8 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -32,10 +31,16 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) -class HeightDialog(ASubWindow, ListedSubWindow): - def __init__(self, title="Height", parent=None): + +class HeightDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Height" + _pamhyr_name = "Height" + + def __init__(self, parent=None): super(HeightDialog, self).__init__( - name=title, ui="InitialConditions_Dialog_Generator_Height", parent=parent + title=self._pamhyr_name, + options=[], + parent=parent ) self.value = None diff --git a/src/View/InitialConditions/PlotDKP.py b/src/View/InitialConditions/PlotDKP.py index 58a83219..ad854fb4 100644 --- a/src/View/InitialConditions/PlotDKP.py +++ b/src/View/InitialConditions/PlotDKP.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,8 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotDKP(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotDKP, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) +class PlotDKP(PamhyrPlot): @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -42,19 +36,19 @@ class PlotDKP(APlot): return self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Elevation (m)"), - color='green', fontsize=11 + self._trad["elevation"], + color='black', fontsize=11 ) self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "KP (m)"), - color='green', fontsize=11 + self._trad["kp"], + color='black', fontsize=11 ) kp = self.data.reach.reach.get_kp() z_min = self.data.reach.reach.get_z_min() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) self.line_kp_zmin = self.canvas.axes.plot( @@ -80,6 +74,6 @@ class PlotDKP(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/InitialConditions/PlotDischarge.py b/src/View/InitialConditions/PlotDischarge.py index 1fe05def..b5a69d5c 100644 --- a/src/View/InitialConditions/PlotDischarge.py +++ b/src/View/InitialConditions/PlotDischarge.py @@ -17,22 +17,10 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot -from PyQt5.QtCore import ( - QCoreApplication -) - -_translate = QCoreApplication.translate - -class PlotDischarge(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotDischarge, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) +class PlotDischarge(PamhyrPlot): @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -42,18 +30,18 @@ class PlotDischarge(APlot): return self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Discharge (m^3/s)"), - color='green', fontsize=11 + self._trad["discharge"], + color='black', fontsize=11 ) self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "KP (m)"), - color='green', fontsize=11 + self._trad["kp"], + color='black', fontsize=11 ) kp = self.data.reach.reach.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) if len(self.data) != 0: @@ -74,6 +62,6 @@ class PlotDischarge(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py index 5e704ba9..aef6a992 100644 --- a/src/View/InitialConditions/Table.py +++ b/src/View/InitialConditions/Table.py @@ -33,18 +33,19 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.InitialConditions.UndoCommand import ( SetCommand, AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, DuplicateCommand, GenerateCommand, ) -from View.InitialConditions.translate import * - logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): def __init__(self, reach=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) @@ -78,8 +79,9 @@ class ComboBoxDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() @@ -87,29 +89,13 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, river=None, reach=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._river = river +class InitialConditionTableModel(PamhyrTableModel): + def __init__(self, reach=None, **kwargs): self._reach = reach - self._undo = undo - self._ics = self._river.initial_conditions.get(reach) + super(InitialConditionTableModel, self).__init__(**kwargs) - def flags(self, index): - column = index.column() - - options = Qt.ItemIsSelectable - if self._headers[column] != "speed": - options |= Qt.ItemIsEnabled | Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._ics) - - def columnCount(self, parent): - return len(self._headers) + def _setup_lst(self): + self._lst = self._data.river.initial_conditions.get(self._reach) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -119,16 +105,10 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] not in ["name", "comment"]: - v = self._ics.get(row)[self._headers[column]] + v = self._lst.get(row)[self._headers[column]] return f"{v:.4f}" else: - return self._ics.get(row)[self._headers[column]] - - return QVariant() - - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[section]] + return self._lst.get(row)[self._headers[column]] return QVariant() @@ -143,7 +123,7 @@ class TableModel(QAbstractTableModel): if self._headers[column] is not None: self._undo.push( SetCommand( - self._ics, row, self._headers[column], value + self._lst, row, self._headers[column], value ) ) except Exception as e: @@ -158,7 +138,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._ics, row + self._lst, row ) ) @@ -170,7 +150,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._ics, rows + self._lst, rows ) ) @@ -182,7 +162,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SortCommand( - self._ics, False + self._lst, False ) ) @@ -199,7 +179,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._ics, "up", row + self._lst, "up", row ) ) @@ -207,7 +187,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._ics): + if row > len(self._lst): return target = row @@ -216,7 +196,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._ics, "down", row + self._lst, "down", row ) ) @@ -234,7 +214,7 @@ class TableModel(QAbstractTableModel): def generate(self, generator, param): self._undo.push( GenerateCommand( - self._ics, generator, param + self._lst, generator, param ) ) self.layoutChanged.emit() diff --git a/src/View/InitialConditions/UndoCommand.py b/src/View/InitialConditions/UndoCommand.py index c96f90fa..72054009 100644 --- a/src/View/InitialConditions/UndoCommand.py +++ b/src/View/InitialConditions/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.InitialConditions.InitialConditions import InitialConditions from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict + class SetCommand(QUndoCommand): def __init__(self, ics, row, column, new_value): QUndoCommand.__init__(self) @@ -47,6 +48,7 @@ class SetCommand(QUndoCommand): def redo(self): self._ics.get(self._row)[self._column] = self._new + class AddCommand(QUndoCommand): def __init__(self, ics, index): QUndoCommand.__init__(self) @@ -64,6 +66,7 @@ class AddCommand(QUndoCommand): else: self._ics.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, ics, rows): QUndoCommand.__init__(self) @@ -83,6 +86,7 @@ class DelCommand(QUndoCommand): def redo(self): self._ics.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, ics, _reverse): QUndoCommand.__init__(self) @@ -168,6 +172,7 @@ class DuplicateCommand(QUndoCommand): for ic in self._ics: self._ics.insert(self._rows[0], ic) + class GenerateCommand(QUndoCommand): def __init__(self, ics, generator, param): QUndoCommand.__init__(self) diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 00e7b121..a0911c4b 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -46,14 +45,16 @@ from View.InitialConditions.UndoCommand import ( DuplicateCommand, ) -from View.InitialConditions.Table import TableModel, ComboBoxDelegate +from View.InitialConditions.Table import ( + InitialConditionTableModel, ComboBoxDelegate, +) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.InitialConditions.PlotDKP import PlotDKP from View.InitialConditions.PlotDischarge import PlotDischarge -from View.InitialConditions.translate import table_headers, retranslate +from View.InitialConditions.translate import ICTranslate from View.InitialConditions.DialogHeight import HeightDialog from View.InitialConditions.DialogDischarge import DischargeDialog @@ -61,67 +62,66 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Initial condition", - study=None, parent=None): - self._study = study - self._reach = study.river.current_reach() - self._ics = self._study.river.initial_conditions.get(self._reach) - self.setup_title(title) +class InitialConditionsWindow(PamhyrWindow): + _pamhyr_ui = "InitialConditions" + _pamhyr_name = "Initial condition" - super(InitialConditionsWindow, self).__init__( - name=self._title, ui="InitialConditions", parent=parent + def __init__(self, study=None, config=None, reach=None, parent=None): + if reach is not None: + self._reach = reach + else: + self._reach = study.river.current_reach() + + name = ( + self._pamhyr_name + + " - " + study.name + + " - " + self._reach.name ) - self.setup_sc() + super(InitialConditionsWindow, self).__init__( + title=name, + study=study, + config=config, + trad=ICTranslate(), + parent=parent + ) + + # Add reach to hash computation data + self._hash_data.append(self._reach) + + self._ics = study.river.initial_conditions.get(self._reach) + self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() - table = self.find(QTableView, f"tableView") - self._table = TableModel( - river = self._study.river, - reach = self._reach, - undo = self._undo_stack, - ) - table.setModel(self._table) - self._delegate_kp = ComboBoxDelegate( - reach = self._reach, + reach=self._reach, parent=self ) - table.setItemDelegateForColumn( - list(table_headers).index("kp"), - self._delegate_kp + self._table = InitialConditionTableModel( + reach=self._reach, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["kp", "discharge", "elevation", "height"], + delegates={"kp": self._delegate_kp}, + data=self._study, + undo=self._undo_stack, + trad=self._trad ) + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): + def setup_plot(self): self.canvas_1 = MplCanvas(width=5, height=4, dpi=100) self.canvas_1.setObjectName("canvas_1") self.toolbar_1 = PamhyrPlotToolbar( @@ -132,9 +132,11 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout_1.addWidget(self.canvas_1) self.plot_1 = PlotDKP( - canvas = self.canvas_1, - data = self._ics, - toolbar = self.toolbar_1, + canvas=self.canvas_1, + data=self._ics, + trad=self._trad, + toolbar=self.toolbar_1, + parent=self ) self.plot_1.draw() @@ -148,9 +150,11 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout_2.addWidget(self.canvas_2) self.plot_2 = PlotDischarge( - canvas = self.canvas_2, - data = self._ics, - toolbar = self.toolbar_2, + canvas=self.canvas_2, + data=self._ics, + trad=self._trad, + toolbar=self.toolbar_2, + parent=self ) self.plot_2.draw() @@ -167,11 +171,6 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self.generate_discharge ) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self._update_plot) def index_selected_row(self): @@ -227,19 +226,19 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) self._update_plot() - def copy(self): + def _copy(self): logger.info("TODO: copy") self._update_plot() - def paste(self): + def _paste(self): logger.info("TODO: paste") self._update_plot() - def undo(self): + def _undo(self): self._table.undo() self._update_plot() - def redo(self): + def _redo(self): self._table.redo() self._update_plot() diff --git a/src/View/InitialConditions/translate.py b/src/View/InitialConditions/translate.py index 54f194e9..00cdf362 100644 --- a/src/View/InitialConditions/translate.py +++ b/src/View/InitialConditions/translate.py @@ -18,23 +18,27 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - # "name": _translate("LateralContribution", "Name"), - "kp": _translate("LateralContribution", "KP (m)"), - # "speed": _translate("LateralContribution", "Speed (m/s)"), - "discharge": _translate("LateralContribution", "Discharge (m³/s)"), - "elevation": _translate("LateralContribution", "Elevation (m)"), - "height": _translate("LateralContribution", "Height (m)"), - # "comment": _translate("LateralContribution", "Comment"), -} -def retranslate(): - # table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["kp"] = _translate("LateralContribution", "KP (m)") - # table_headers["speed"] = _translate("LateralContribution", "Speed (m/s)") - table_headers["discharge"] = _translate("LateralContribution", "Discharge (m³/s)") - table_headers["elevation"] = _translate("LateralContribution", "Elevation (m)") - table_headers["height"] = _translate("LateralContribution", "Height (m)") - # table_headers["comment"] = _translate("LateralContribution", "Comment") +class ICTranslate(PamhyrTranslate): + def __init__(self): + super(ICTranslate, self).__init__() + + self._dict["elevation"] = _translate( + "InitialCondition", "Elevation (m)") + self._dict["discharge"] = _translate( + "InitialCondition", "Discharge (m³/s)") + self._dict["kp"] = _translate("InitialCondition", "KP (m)") + + self._sub_dict["table_headers"] = { + # "name": _translate("InitialCondition", "Name"), + "kp": _translate("InitialCondition", "KP (m)"), + # "speed": _translate("InitialCondition", "Speed (m/s)"), + "discharge": _translate("InitialCondition", "Discharge (m³/s)"), + "elevation": _translate("InitialCondition", "Elevation (m)"), + "height": _translate("InitialCondition", "Height (m)"), + # "comment": _translate("InitialCondition", "Comment"), + } diff --git a/src/View/LateralContribution/Edit/Plot.py b/src/View/LateralContribution/Edit/Plot.py index 5843ee56..c7e95caa 100644 --- a/src/View/LateralContribution/Edit/Plot.py +++ b/src/View/LateralContribution/Edit/Plot.py @@ -19,7 +19,7 @@ from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,15 +29,20 @@ from View.LateralContribution.Edit.translate import * _translate = QCoreApplication.translate -class Plot(APlot): - def __init__(self, canvas=None, data=None, - mode = "time", toolbar=None): + +class Plot(PamhyrPlot): + def __init__(self, mode="time", data=None, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) + self._table_headers = self._trad.get_dict("table_headers") self._mode = mode def custom_ticks(self): @@ -64,8 +69,8 @@ class Plot(APlot): map( lambda v: str( datetime.fromtimestamp(v[0]) - t0 - ).split(",")[0]\ - .replace("days", _translate("LateralContribution", "days"))\ + ).split(",")[0] + .replace("days", _translate("LateralContribution", "days")) .replace("day", _translate("LateralContribution", "day")), fx ) @@ -80,7 +85,6 @@ class Plot(APlot): self.canvas.axes.set_xticks(ticks=xx, labels=xt, rotation=45) - @timer def draw(self): self.canvas.axes.cla() @@ -105,22 +109,22 @@ class Plot(APlot): # Plot label header = self.data.header self.canvas.axes.set_xlabel( - table_headers[header[0]], color='black', fontsize=10 + self._table_headers[header[0]], color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - table_headers[header[1]], color='black', fontsize=10 + self._table_headers[header[1]], color='black', fontsize=10 ) self.canvas.axes.autoscale_view(True, True, True) self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() - #self.toolbar.update() + # self.toolbar.update() self._init = True @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 2fff0895..8b43602d 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -22,8 +22,6 @@ from datetime import date, time, datetime, timedelta from tools import trace, timer -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow from View.Tools.PamhyrTable import PamhyrTableModel from PyQt5.QtCore import ( diff --git a/src/View/LateralContribution/Edit/UndoCommand.py b/src/View/LateralContribution/Edit/UndoCommand.py index 715683c9..780ea8ac 100644 --- a/src/View/LateralContribution/Edit/UndoCommand.py +++ b/src/View/LateralContribution/Edit/UndoCommand.py @@ -25,6 +25,7 @@ from PyQt5.QtWidgets import ( from Model.LateralContribution.LateralContribution import LateralContribution + class SetDataCommand(QUndoCommand): def __init__(self, data, index, column, new_value): QUndoCommand.__init__(self) @@ -42,6 +43,7 @@ class SetDataCommand(QUndoCommand): def redo(self): self._data._set_i_c_v(self._index, self._column, self._new) + class AddCommand(QUndoCommand): def __init__(self, data, index): QUndoCommand.__init__(self) @@ -59,6 +61,7 @@ class AddCommand(QUndoCommand): else: self._data.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, data, rows): QUndoCommand.__init__(self) @@ -78,6 +81,7 @@ class DelCommand(QUndoCommand): def redo(self): self._data.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, data, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 53576a6b..897703a4 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -18,8 +18,7 @@ from tools import timer, trace -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate from PyQt5.QtGui import ( @@ -37,78 +36,77 @@ from PyQt5.QtWidgets import ( QHeaderView, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.LateralContribution.translate import long_types -from View.LateralContribution.Edit.translate import table_headers, retranslate +from View.LateralContribution.Edit.translate import LCETranslate from View.LateralContribution.Edit.Table import TableModel from View.LateralContribution.Edit.Plot import Plot _translate = QCoreApplication.translate -class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit lateral contribution", - data=None, study=None, parent=None): - self._data = data - self._study = study - self._title = title - self.compute_title() +class EditLateralContributionWindow(PamhyrWindow): + _pamhyr_ui = "EditLateralContribution" + _pamhyr_name = "Edit lateral contribution" + + def __init__(self, data=None, + study=None, config=None, + parent=None): + self._data = data + trad = LCETranslate() + + name = self._pamhyr_name + if self._data is not None: + edge_name = (self._data.edge.name if self._data.edge is not None + else _translate("LateralContribution", + "Not associate")) + name = ( + _translate("Edit lateral contribution", self._pamhyr_name) + + f" - {study.name} " + + f" - {self._data.name} ({self._data.id}) " + + f"({trad.get_dict('long_types')[self._data.lctype]} - " + + f"{edge_name})" + ) super(EditLateralContributionWindow, self).__init__( - name=title, ui="EditLateralContribution", parent=parent + title=name, + study=study, + config=config, + trad=trad, + parent=parent ) - self.ui.setWindowTitle(self._title) + self._hash_data.append(data) - self.setup_sc() self.setup_table() self.setup_plot() self.setup_connections() - def compute_title(self): - if self._data is not None: - edge_name = (self._data.edge.name if self._data.edge is not None - else _translate("LateralContribution", "Not associate")) - self._title = ( - _translate("Edit lateral contribution", self._title) + - f" - {self._study.name} " + - f" - {self._data.name} ({self._data.id}) " + - f"({long_types[self._data.lctype]} - {edge_name})" - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() + self._delegate_time = PamhyrExTimeDelegate( + data=self._data, + mode=self._study.time_system, + parent=self + ) + headers = {} + table_headers = self._trad.get_dict("table_headers") for h in self._data.header: headers[h] = table_headers[h] - self._delegate_time = PamhyrExTimeDelegate( - data = self._data, - mode = self._study.time_system, - parent = self - ) - table = self.find(QTableView, "tableView") self._table = TableModel( - table_view = table, - table_headers = headers, - editable_headers = self._data.header, - delegates = { - "time": self._delegate_time, + table_view=table, + table_headers=headers, + editable_headers=self._data.header, + delegates={ + # "time": self._delegate_time, }, - data = self._data, - undo = self._undo_stack, - opt_data = self._study.time_system + data=self._data, + undo=self._undo_stack, + trad=self._trad, + opt_data=self._study.time_system ) table.setModel(self._table) @@ -126,24 +124,20 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self.verticalLayout.addWidget(self.canvas) self.plot = Plot( - canvas = self.canvas, - data = self._data, - mode = self._study.time_system, - toolbar = self.toolbar, + canvas=self.canvas, + data=self._data, + mode=self._study.time_system, + toolbar=self.toolbar, + trad=self._trad, + parent=self ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self.update) def update(self): @@ -167,7 +161,6 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): ) ) - def add(self): rows = self.index_selected_rows() if len(self._data) == 0 or len(rows) == 0: @@ -199,8 +192,7 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) self.plot.update() - - def copy(self): + def _copy(self): rows = self.index_selected_rows() table = [] @@ -212,7 +204,7 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -226,10 +218,10 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table.paste(row, header, data) self.plot.update() - def undo(self): + def _undo(self): self._table.undo() self.plot.update() - def redo(self): + def _redo(self): self._table.redo() self.plot.update() diff --git a/src/View/LateralContribution/Edit/translate.py b/src/View/LateralContribution/Edit/translate.py index f04534e0..9fc86e4d 100644 --- a/src/View/LateralContribution/Edit/translate.py +++ b/src/View/LateralContribution/Edit/translate.py @@ -18,21 +18,21 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate +from View.LateralContribution.translate import LCTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("LateralContribution", "X"), - "y": _translate("LateralContribution", "Y"), - "time": _translate("LateralContribution", "Time"), - "date": _translate("LateralContribution", "Date"), - "discharge": _translate("LateralContribution", "Discharge (m³/s)"), - "z": _translate("LateralContribution", "Z (m)") -} -def retranslate(): - table_headers["x"] = _translate("LateralContribution", "X") - table_headers["y"] = _translate("LateralContribution", "Y") - table_headers["time"] = _translate("LateralContribution", "Time") - table_headers["date"] = _translate("LateralContribution", "Date") - table_headers["discharge"] = _translate("LateralContribution", "Discharge (m³/s)") - table_headers["z"] = _translate("LateralContribution", "Z (m)") +class LCETranslate(LCTranslate): + def __init__(self): + super(LCETranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "x": _translate("LateralContribution", "X"), + "y": _translate("LateralContribution", "Y"), + "time": _translate("LateralContribution", "Time"), + "date": _translate("LateralContribution", "Date"), + "discharge": _translate("LateralContribution", "Discharge (m³/s)"), + "z": _translate("LateralContribution", "Z (m)") + } diff --git a/src/View/LateralContribution/Table.py b/src/View/LateralContribution/Table.py index 2a3f4196..060ca8cf 100644 --- a/src/View/LateralContribution/Table.py +++ b/src/View/LateralContribution/Table.py @@ -46,22 +46,26 @@ from Model.LateralContribution.LateralContributionTypes import ( ) from View.Tools.PamhyrTable import PamhyrTableModel -from View.LateralContribution.translate import * +from View.LateralContribution.translate import LC_types logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", tab="", parent=None): + def __init__(self, data=None, mode="type", tab="", + trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data self._mode = mode self._tab = tab + self._trad = trad def createEditor(self, parent, option, index): self.editor = QComboBox(parent) + long_types = self._trad.get_dict("long_types") if self._mode == "type": lst = list( @@ -95,8 +99,9 @@ class ComboBoxDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() @@ -108,6 +113,7 @@ class TableModel(PamhyrTableModel): def _setup_lst(self): self._lst = self._data.lateral_contribution self._tab = self._opt_data + self._long_types = self._trad.get_dict("long_types") def rowCount(self, parent): return self._lst.len(self._tab) @@ -123,7 +129,7 @@ class TableModel(PamhyrTableModel): return self._lst.get(self._tab, row).name elif self._headers[column] == "type": t = self._lst.get(self._tab, row).lctype - return long_types[t] + return self._long_types[t] elif self._headers[column] == "edge": n = self._lst.get(self._tab, row).edge if n is None: @@ -151,7 +157,8 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() + if v == value) self._undo.push( SetTypeCommand( self._lst, self._tab, row, LC_types[key] diff --git a/src/View/LateralContribution/UndoCommand.py b/src/View/LateralContribution/UndoCommand.py index 3d858436..382ae1ac 100644 --- a/src/View/LateralContribution/UndoCommand.py +++ b/src/View/LateralContribution/UndoCommand.py @@ -24,7 +24,10 @@ from PyQt5.QtWidgets import ( ) from Model.LateralContribution.LateralContribution import LateralContribution -from Model.LateralContribution.LateralContributionList import LateralContributionList +from Model.LateralContribution.LateralContributionList import ( + LateralContributionList +) + class SetNameCommand(QUndoCommand): def __init__(self, lcs, tab, index, new_value): @@ -42,6 +45,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._lcs.get(self._tab, self._index).name = self._new + class SetBeginCommand(QUndoCommand): def __init__(self, lcs, tab, index, new_value): QUndoCommand.__init__(self) @@ -58,6 +62,7 @@ class SetBeginCommand(QUndoCommand): def redo(self): self._lcs.get(self._tab, self._index).begin_kp = float(self._new) + class SetEndCommand(QUndoCommand): def __init__(self, lcs, tab, index, new_value): QUndoCommand.__init__(self) @@ -91,6 +96,7 @@ class SetEdgeCommand(QUndoCommand): def redo(self): self._lcs.get(self._tab, self._index).edge = self._new + class SetTypeCommand(QUndoCommand): def __init__(self, lcs, tab, index, _type): QUndoCommand.__init__(self) @@ -109,6 +115,7 @@ class SetTypeCommand(QUndoCommand): def redo(self): self._lcs.set(self._tab, self._index, self._new) + class AddCommand(QUndoCommand): def __init__(self, lcs, tab, index): QUndoCommand.__init__(self) @@ -127,6 +134,7 @@ class AddCommand(QUndoCommand): else: self._lcs.insert(self._tab, self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, lcs, tab, rows): QUndoCommand.__init__(self) @@ -147,6 +155,7 @@ class DelCommand(QUndoCommand): def redo(self): self._lcs.delete_i(self._tab, self._rows) + class SortCommand(QUndoCommand): def __init__(self, lcs, tab, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 31f82906..a6f04af5 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -54,74 +53,69 @@ from View.LateralContribution.Table import ( TableModel, ComboBoxDelegate ) -from View.Plot.MplCanvas import MplCanvas +from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Geometry.PlotXY import PlotXY from View.LateralContribution.translate import ( - long_types, table_headers, LC_types, - retranslate, + LC_types, LCTranslate, ) from View.LateralContribution.Edit.Window import EditLateralContributionWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() -class LateralContributionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Lateral contribution", study=None, parent=None): - self._title = title + " - " + study.name + +class LateralContributionWindow(PamhyrWindow): + _pamhyr_ui = "LateralContributions" + _pamhyr_name = "Lateral contribution" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(LateralContributionWindow, self).__init__( - name=title, ui="LateralContributions", parent=parent + title=name, + study=study, + trad=LCTranslate(), + config=config, + parent=parent ) - self._study = study self._lcs = self._study.river.lateral_contribution - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() self._table = {} for t in ["liquid", "solid", "suspenssion"]: self._delegate_type = ComboBoxDelegate( - data = self._study.river, - mode = "type", - tab = t, + data=self._study.river, + mode="type", + tab=t, + trad=self._trad, parent=self ) self._delegate_edge = ComboBoxDelegate( - data = self._study.river, - mode = "edge", - tab = t, + data=self._study.river, + mode="edge", + tab=t, + trad=self._trad, parent=self ) table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - table_view = table, - table_headers = table_headers, - editable_headers = True, - delegates = { + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=self._trad.get_dict("table_headers"), + delegates={ "type": self._delegate_type, "edge": self._delegate_edge, }, - data = self._study.river, - undo = self._undo_stack, - opt_data = t, + data=self._study.river, + undo=self._undo_stack, + trad=self._trad, + opt_data=t, ) table.setModel(self._table[t]) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -135,9 +129,9 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self.plot_layout.addWidget(self.canvas) self.plot = PlotXY( - canvas = self.canvas, - data = None, - toolbar = None, + canvas=self.canvas, + data=None, + toolbar=None, ) def setup_connections(self): @@ -146,11 +140,6 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_edit").triggered.connect(self.edit) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - for t in ["liquid", "solid", "suspenssion"]: table = self.find(QTableView, f"tableView_{t}") table.selectionModel()\ @@ -205,9 +194,9 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): highlight = (lc.begin_kp, lc.end_kp) self.plot = PlotXY( - canvas = self.canvas, - data = data, - toolbar = None, + canvas=self.canvas, + data=data, + toolbar=None, ) self.plot.draw(highlight=highlight) @@ -247,20 +236,20 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table[tab].move_down(row) self._set_current_reach() - def copy(self): + def _copy(self): logger.info("TODO: copy") self._set_current_reach() - def paste(self): + def _paste(self): logger.info("TODO: paste") self._set_current_reach() - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() self._set_current_reach() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() self._set_current_reach() @@ -269,17 +258,17 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): tab = self.current_tab() rows = self.index_selected_rows() for row in rows: - win = self.sub_win_filter_first( - "Edit lateral contribution", - contain = [f"({self._lcs.get(tab, row).id})"] - ) + data = self._lcs.get(tab, row) - if win is None: - win = EditLateralContributionWindow( - data=self._lcs.get(tab, row), - study=self._study, - parent=self - ) - win.show() - else: - win.activateWindow() + if self.sub_window_exists( + EditLateralContributionWindow, + data=[self._study, None, data] + ): + continue + + win = EditLateralContributionWindow( + data=data, + study=self._study, + parent=self + ) + win.show() diff --git a/src/View/LateralContribution/translate.py b/src/View/LateralContribution/translate.py index cf430824..f005a063 100644 --- a/src/View/LateralContribution/translate.py +++ b/src/View/LateralContribution/translate.py @@ -18,27 +18,14 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + from Model.LateralContribution.LateralContributionTypes import ( NotDefined, LateralContrib, Rain, Evaporation, ) _translate = QCoreApplication.translate -long_types = { - "ND": _translate("LateralContribution", "Not defined"), - "LC": _translate("LateralContribution", "Lateral contribution"), - "RA": _translate("LateralContribution", "Rain"), - "EV": _translate("LateralContribution", "Evaporation"), -} - -table_headers = { - "name": _translate("LateralContribution", "Name"), - "type": _translate("LateralContribution", "Type"), - "edge": _translate("LateralContribution", "Reach"), - "begin_kp": _translate("LateralContribution", "Begin kp (m)"), - "end_kp": _translate("LateralContribution", "End kp (m)") -} - LC_types = { "ND": NotDefined, "LC": LateralContrib, @@ -46,14 +33,22 @@ LC_types = { "EV": Evaporation, } -def retranslate(): - table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["type"] = _translate("LateralContribution", "Type") - table_headers["edge"] = _translate("LateralContribution", "Reach") - table_headers["begin_kp"] = _translate("LateralContribution", "Begin kp (m)") - table_headers["end_kp"] = _translate("LateralContribution", "End kp (m)") - long_types["ND"] = _translate("LateralContribution", "Not defined") - long_types["LC"] = _translate("LateralContribution", "Lateral contribution") - long_types["RA"] = _translate("LateralContribution", "Rain") - long_types["EV"] = _translate("LateralContribution", "Evaporation") +class LCTranslate(PamhyrTranslate): + def __init__(self): + super(LCTranslate, self).__init__() + + self._sub_dict["long_types"] = { + "ND": _translate("LateralContribution", "Not defined"), + "LC": _translate("LateralContribution", "Lateral contribution"), + "RA": _translate("LateralContribution", "Rain"), + "EV": _translate("LateralContribution", "Evaporation"), + } + + self._sub_dict["table_headers"] = { + "name": _translate("LateralContribution", "Name"), + "type": _translate("LateralContribution", "Type"), + "edge": _translate("LateralContribution", "Reach"), + "begin_kp": _translate("LateralContribution", "Begin kp (m)"), + "end_kp": _translate("LateralContribution", "End kp (m)") + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index d5a9d78d..e48b4803 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -17,27 +17,28 @@ # -*- coding: utf-8 -*- import os +import sys import logging import subprocess from queue import Queue from PyQt5 import QtGui from PyQt5.QtGui import ( - QKeySequence, + QKeySequence, QDesktopServices, ) from PyQt5.QtCore import ( - QTranslator, QEvent + Qt, QTranslator, QEvent, QUrl, ) from PyQt5.QtWidgets import ( QMainWindow, QApplication, QAction, QFileDialog, QShortcut, QMenu, QToolBar, - QMessageBox, + QMessageBox, QProgressDialog, ) from PyQt5.uic import loadUi -from View.ASubWindow import WindowToolKit -from View.ListedSubWindow import ListedSubWindow +from View.Tools.ASubWindow import WindowToolKit +from View.Tools.ListedSubWindow import ListedSubWindow from View.DummyWindow import DummyWindow from View.Configure.Window import ConfigureWindow @@ -46,6 +47,8 @@ from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow +from View.Reservoir.Window import ReservoirWindow +from View.HydraulicStructures.Window import HydraulicStructuresWindow from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow @@ -58,6 +61,15 @@ from View.CheckList.Window import CheckListWindow from View.Results.Window import ResultsWindow from View.Debug.Window import ReplWindow +# Optional internal display of documentation for make the application +# package lighter... +try: + from View.Doc.Window import DocWindow + _doc = "internal" +except Exception as e: + print("Handle exception: {e}") + _doc = "external" + from Model.Study import Study logger = logging.getLogger() @@ -91,13 +103,15 @@ define_model_action = [ "action_menu_boundary_conditions", "action_menu_initial_conditions", "action_menu_edit_friction", "action_menu_edit_lateral_contribution", "action_menu_run_solver", "action_menu_sediment_layers", - "action_menu_edit_reach_sediment_layers" + "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs", + "action_menu_edit_hydraulic_structures" ] action = ( no_model_action + model_action + define_model_action ) + class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def __init__(self, conf=None): super(ApplicationWindow, self).__init__() @@ -108,9 +122,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.conf = conf # Model - self.model = None + self._study = None # Results + self._last_solver = None self._last_results = None # UI @@ -122,25 +137,29 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.setup_sc() self.setup_connection() self.default_style() - self.setup_debug_mode(init = True) + self.setup_debug_mode(init=True) self.trans = QTranslator(self) - #self.ui.retranslateUi() + # self.ui.retranslateUi() - if self.conf.last_study != "" and not self.conf.close_correctly: - self.dialog_reopen_study() + if not self.conf.close_correctly: + if self.conf.last_study != "": + self.dialog_reopen_study() + + if _doc == "external": + logger.info("doc: Internal documentation is not available...") def set_title(self): title = "(dbg) " if self.conf.debug else "" - if self.model is not None: - title += f"Pamhyr2 - {self.model.name}" + if self._study is not None: + title += f"Pamhyr2 - {self._study.name}" self.setWindowTitle(title) else: title += "Pamhyr2" self.setWindowTitle(title) - def enable_actions(self, action:str, enable:bool): + def enable_actions(self, action: str, enable: bool): """Enable of disable an action componant Args: @@ -150,6 +169,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ + logger.debug(f"Set {action} to {enable}") self.findChild(QAction, action).setEnabled(enable) def setup_sc(self): @@ -174,14 +194,28 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_network": self.open_network, "action_menu_edit_geometry": self.open_geometry, "action_menu_boundary_conditions": self.open_boundary_cond, + "action_menu_edit_reservoirs": self.open_reservoir, + "action_menu_edit_hydraulic_structures": + self.open_hydraulic_structures, "action_menu_initial_conditions": self.open_initial_conditions, "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, "action_menu_run_solver": self.run_solver, "action_menu_sediment_layers": self.open_sediment_layers, - "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, + "action_menu_edit_reach_sediment_layers": + self.open_reach_sediment_layers, "action_menu_close": self.close_model, - ## Help + "action_menu_results_last": self.open_last_results, + # Help + "action_menu_pamhyr_users_pdf": + lambda: self.open_doc_user(ext="pdf"), + "action_menu_pamhyr_developers_pdf": + lambda: self.open_doc_dev(ext="pdf"), + "action_menu_pamhyr_users_html": + lambda: self.open_doc_user(ext="html"), + "action_menu_pamhyr_developers_html": + lambda: self.open_doc_dev(ext="html"), + "action_menu_mage": self.open_doc_mage, "action_menu_about": self.open_about, # ToolBar action "action_toolBar_quit": self.close, @@ -189,7 +223,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_toolBar_save": self.save_study, "action_toolBar_close": self.close_model, "action_toolBar_run_solver": self.run_solver, - ## Current actions + # Current actions "action_toolBar_network": self.open_network, "action_toolBar_geometry": self.open_geometry, "action_toolBar_mesh": lambda: self.open_dummy("Mesh"), @@ -217,7 +251,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): super(ApplicationWindow, self).changeEvent(event) def close(self): - if self.model is not None and not self.model.is_saved: + if self._study is not None and not self._study.is_saved: self._close_question = True if self.dialog_close(): # PAMHYR is close correctly (no crash) @@ -234,8 +268,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def closeEvent(self, event): if not self._close_question: - if self.model is not None and not self.model.is_saved: - if self.dialog_close(cancel = False): + if self._study is not None and not self._study.is_saved: + if self.dialog_close(cancel=False): # PAMHYR is close correctly (no crash) self.conf.set_close_correctly() super(ApplicationWindow, self).closeEvent(event) @@ -249,10 +283,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Nothing """ self.update_enable_action() - # Maximise window - self.showMaximized() + # self.showMaximized() - def set_debug_lvl(self, debug = True): + def set_debug_lvl(self, debug=True): if debug: logger.setLevel(logging.DEBUG) logger.info("Set logging level to DEBUG") @@ -260,7 +293,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): logger.setLevel(logging.INFO) logger.info("Set logging level to INFO") - def setup_debug_mode(self, init = False): + def setup_debug_mode(self, init=False): menu = self.findChild(QMenu, "menu_help") self.set_title() @@ -270,38 +303,39 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.debug_action.triggered.connect(self.open_debug) self.debug_sqlite_action = QAction("Debug SQLite", self) - self.debug_sqlite_action.setStatusTip("Open SQLite debuging tool (sqlitebrowser)") + self.debug_sqlite_action.setStatusTip( + "Open SQLite debuging tool (sqlitebrowser)") self.debug_sqlite_action.triggered.connect(self.open_sqlite) if self.conf.debug: menu.addAction(self.debug_action) menu.addAction(self.debug_sqlite_action) - self.set_debug_lvl(debug = True) + self.set_debug_lvl(debug=True) else: if self.conf.debug: menu.addAction(self.debug_action) menu.addAction(self.debug_sqlite_action) - self.set_debug_lvl(debug = True) + self.set_debug_lvl(debug=True) else: menu.removeAction(self.debug_action) menu.removeAction(self.debug_sqlite_action) - self.set_debug_lvl(debug = False) + self.set_debug_lvl(debug=False) ######### # MODEL # ######### def get_model(self): - return self.model + return self._study def set_model(self, model): - self.model = model + self._study = model self.update_enable_action() - self.conf.set_last_study(self.model.filename) + self.conf.set_last_study(self._study.filename) self.set_title() def close_model(self): - self.model = None + self._study = None self.update_enable_action() self.conf.set_close_correctly() self.set_title() @@ -315,7 +349,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - no_model = self.model is None + no_model = self._study is None for action in no_model_action: self.enable_actions(action, no_model) @@ -326,9 +360,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): for action in model_action: self.enable_actions(action, not no_model) - def set_results(self, results): + def set_results(self, solver, results): + self._last_solver = solver self._last_results = results + self.enable_actions("action_menu_results_last", True) + ############ # FEATURES # ############ @@ -343,7 +380,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Nothing """ self.set_model(Study.open(filename)) - logger.info(f"Open Study - {self.model.name}") + logger.info(f"Open Study - {self._study.name}") self.set_title() def save_study(self): @@ -355,22 +392,34 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model.filename is None or self.model.filename == "": + if self._study.filename is None or self._study.filename == "": file_name, _ = QFileDialog.getSaveFileName( self, "Save File", "", "Pamhyr(*.pamhyr)" ) if file_name.rsplit(".", 1)[-1] == "pamhyr": - self.model.filename = file_name + self._study.filename = file_name else: - self.model.filename = file_name + ".pamhyr" + self._study.filename = file_name + ".pamhyr" - if self.model.is_saved: + if self._study.is_saved: return + sql_request_count = self._study.sql_save_request_count() + progress = QProgressDialog( + "Saving...", None, + 0, sql_request_count, + parent=self + ) + progress.setWindowModality(Qt.WindowModal) + progress.setValue(0) + logger.info("Save...") - self.model.save() + self._study.save( + progress=lambda: progress.setValue(progress.value() + 1) + ) + logger.info("Done") def save_as_study(self): """Save current study as new file @@ -386,12 +435,25 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "", "Pamhyr(*.pamhyr)" ) - if file_name[-4:] == ".pamhyr": - self.model.filename = file_name + if file_name.rsplit(".", 1)[-1] == ".pamhyr": + self._study.filename = file_name else: - self.model.filename = file_name + ".pamhyr" + self._study.filename = file_name + ".pamhyr" - self.model.save() + sql_request_count = self._study.sql_save_request_count() + progress = QProgressDialog( + "Saving...", None, + 0, sql_request_count, + parent=self + ) + progress.setWindowModality(Qt.WindowModal) + progress.setValue(0) + + logger.info("Save...") + self._study.save( + progress=lambda: progress.setValue(progress.value() + 1) + ) + logger.info("Done") ################## # MSG AND DIALOG # @@ -407,7 +469,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): dlg.setWindowTitle("Last open study") dlg.setText("Do you want to open again the last open study?") - opt = QMessageBox.Cancel | QMessageBox.Ok #| QMessageBox.Open + opt = QMessageBox.Cancel | QMessageBox.Ok # | QMessageBox.Open dlg.setStandardButtons(opt) dlg.setIcon(QMessageBox.Question) @@ -423,7 +485,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): elif res == QMessageBox.Cancel: return False - def dialog_close(self, cancel = True): + def dialog_close(self, cancel=True): dlg = QMessageBox(self) dlg.setWindowTitle("Close PAMHYR without saving study") @@ -457,7 +519,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - self.config = ConfigureWindow(conf=self.conf, parent=self) + if self.sub_window_exists( + ConfigureWindow, + data=[None, self.conf] + ): + return + + self.config = ConfigureWindow(config=self.conf, parent=self) self.config.show() def open_about(self): @@ -468,6 +536,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ + if self.sub_window_exists( + AboutWindow, + data=[None, None] + ): + return + self.about = AboutWindow(parent=self) self.about.show() @@ -477,11 +551,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is None: + if self._study is None: dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.FileMode.ExistingFile) dialog.setDefaultSuffix(".pamhyr") - #dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden) + # dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden) dialog.setNameFilters(['PamHyr (*.pamhyr)']) dialog.setDirectory(os.path.dirname(self.conf.last_study)) @@ -495,7 +569,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is None: + if self._study is None: + if self.sub_window_exists( + NewStudyWindow, + data=[None, None] + ): + return + self.new_study = NewStudyWindow(parent=self) self.new_study.show() @@ -505,8 +585,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if not self.model is None: - self.new_study = NewStudyWindow(study=self.model, parent=self) + if self._study is not None: + if self.sub_window_exists( + NewStudyWindow, + data=[self._study, None] + ): + return + + self.new_study = NewStudyWindow(study=self._study, parent=self) self.new_study.show() def open_network(self): @@ -515,12 +601,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is not None: - if not self.sub_win_exists("River network"): - self.network = NetworkWindow(model=self.model, parent=self) - self.network.show() - else: - self.network.activateWindow() + if self._study is not None: + if self.sub_window_exists( + NetworkWindow, + data=[self._study, None] + ): + return + + self.network = NetworkWindow(study=self._study, parent=self) + self.network.show() def open_geometry(self): """Open geometry window @@ -528,156 +617,206 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if (self.model is not None and self.model.river.has_current_reach()): - geometry = self.sub_win_filter_first( - "Geometry", - contain = [self.model.river.current_reach().name] - ) + if (self._study is not None and self._study.river.has_current_reach()): + reach = self._study.river.current_reach().reach - if geometry is None: - geometry = GeometryWindow(model=self.model, parent=self) - geometry.show() - else: - geometry.activateWindow() + if self.sub_window_exists( + GeometryWindow, + data=[self._study, self.conf, reach] + ): + return + + geometry = GeometryWindow( + study=self._study, + config=self.conf, + reach=reach, + parent=self + ) + geometry.show() else: self.msg_select_reach() def open_boundary_cond(self): - bound = self.sub_win_filter_first( - "Boundary conditions", - contain = [] - ) + if self.sub_window_exists( + BoundaryConditionWindow, + data=[self._study, None] + ): + return - if bound is None: - bound = BoundaryConditionWindow(study = self.model, parent = self) - bound.show() - else: - bound.activateWindow() + bound = BoundaryConditionWindow(study=self._study, parent=self) + bound.show() + + def open_reservoir(self): + if self.sub_window_exists( + ReservoirWindow, + data=[self._study, None] + ): + return + + reservoir = ReservoirWindow(study=self._study, parent=self) + reservoir.show() + + def open_hydraulic_structures(self): + if self.sub_window_exists( + HydraulicStructuresWindow, + data=[self._study, None] + ): + return + + hydraulic_structures = HydraulicStructuresWindow( + study=self._study, + parent=self + ) + hydraulic_structures.show() def open_lateral_contrib(self): - lateral = self.sub_win_filter_first( - "Lateral contribution", - contain = [] - ) + if self.sub_window_exists( + LateralContributionWindow, + data=[self._study, None] + ): + return - if lateral is None: - lateral = LateralContributionWindow(study = self.model, parent = self) - lateral.show() - else: - lateral.activateWindow() + lateral = LateralContributionWindow( + study=self._study, + parent=self + ) + lateral.show() def open_stricklers(self): - strick = self.sub_win_filter_first( - "Stricklers", - contain = [] - ) + if self.sub_window_exists( + StricklersWindow, + data=[self._study, self.conf] + ): + return - if strick is None: - strick = StricklersWindow( - study = self.model, - config = self.conf, - parent = self - ) - strick.show() - else: - strick.activateWindow() + strick = StricklersWindow( + study=self._study, + config=self.conf, + parent=self + ) + strick.show() def open_frictions(self): - if (self.model is not None and - self.model.river.has_current_reach()): + if self._study is not None: + if self._study.river.has_current_reach(): + reach = self._study.river.current_reach() - frictions = self.sub_win_filter_first( - "Frictions", - contain = [self.model.river.current_reach().name] - ) + if self.sub_window_exists( + FrictionsWindow, + data=[self._study, None, reach] + ): + return - if frictions is None: frictions = FrictionsWindow( - study = self.model, - parent = self + study=self._study, + parent=self ) frictions.show() else: - frictions.activateWindow() - else: - self.msg_select_reach() + self.msg_select_reach() def open_initial_conditions(self): - if self.model.river.has_current_reach(): - initial = self.sub_win_filter_first( - "Initial condition", - contain = [self.model.river.current_reach().name] - ) + if self._study.river.has_current_reach(): + reach = self._study.river.current_reach() - if initial is None: - initial = InitialConditionsWindow( - study = self.model, - parent = self - ) - initial.show() - else: - initial.activateWindow() + if self.sub_window_exists( + InitialConditionsWindow, + data=[self._study, self.conf, reach] + ): + return + + initial = InitialConditionsWindow( + study=self._study, + config=self.conf, + reach=reach, + parent=self + ) + initial.show() else: self.msg_select_reach() def open_solver_parameters(self): - params = self.sub_win_filter_first( - "Solver parameters", - contain = [] - ) + if self.sub_window_exists( + SolverParametersWindow, + data=[self._study, None] + ): + return - if params is None: - params = SolverParametersWindow( - study = self.model, - parent = self - ) - params.show() - else: - params.activateWindow() + params = SolverParametersWindow( + study=self._study, + parent=self + ) + params.show() def open_sediment_layers(self): + if self.sub_window_exists( + SedimentLayersWindow, + data=[self._study, None] + ): + return + sl = SedimentLayersWindow( - study = self.model, - parent = self + study=self._study, + parent=self ) sl.show() def open_reach_sediment_layers(self): + reach = self._study.river.current_reach().reach + + if self.sub_window_exists( + ReachSedimentLayersWindow, + data=[self._study, None, reach] + ): + return + sl = ReachSedimentLayersWindow( - study = self.model, - parent = self + study=self._study, + reach=reach, + parent=self ) sl.show() def run_solver(self): - if self.model is None: + if self._study is None: return run = SelectSolverWindow( - study = self.model, - config = self.conf, - parent = self + study=self._study, + config=self.conf, + parent=self ) if run.exec(): solver = run.solver + + if self.sub_window_exists( + CheckListWindow, + data=[ + self._study, + self.conf, + solver + ] + ): + return + check = CheckListWindow( - study = self.model, - config = self.conf, - solver = solver, - parent = self + study=self._study, + config=self.conf, + solver=solver, + parent=self ) check.show() def solver_log(self, solver): sol = SolverLogWindow( - study = self.model, - config = self.conf, - solver = solver, - parent = self + study=self._study, + config=self.conf, + solver=solver, + parent=self ) sol.show() - def open_solver_results(self, solver, results = None): + def open_solver_results(self, solver, results=None): # If no specific results, get last results if results is None: results = self._last_results @@ -687,21 +826,74 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): return # Windows already opened - res = self.sub_win_filter_first( - "Results", - contain = [solver.name, results.date] + if self.sub_window_exists( + ResultsWindow, + data=[ + self._study, + None, # No config + solver, + results + ] + ): + return + + res = ResultsWindow( + study=self._study, + solver=solver, + results=results, + parent=self + ) + res.show() + + def open_last_results(self): + if self._last_solver is None or self._last_results is None: + return + + self.open_solver_results(self._last_solver, self._last_results) + + ################# + # DOCUMENTATION # + ################# + + def _doc_path_file(self, filename): + if ".py" in sys.argv[0]: + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "doc", filename + ) + ) + + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "doc", filename + ) ) - if res is None: - res = ResultsWindow( - study = self.model, - solver = solver, - results = results, - parent = self - ) - res.show() + def open_doc(self, filename): + if _doc == "external": + url = QUrl(f"file://{self._doc_path_file(filename)}") + QDesktopServices.openUrl(url) else: - res.activateWindow() + if ".odt" in filename: + url = QUrl(f"file://{DocWindow._path_file(filename)}") + QDesktopServices.openUrl(url) + else: + doc = DocWindow( + filename=filename, + parent=self + ) + doc.show() + + def open_doc_user(self, ext="pdf"): + self.open_doc(f"Pamhyr2-users.{ext}") + + def open_doc_dev(self, ext="pdf"): + self.open_doc(f"Pamhyr2-dev.{ext}") + + def open_doc_mage(self): + self.open_doc("Mage-8.odt") ######### # DEBUG # @@ -709,18 +901,18 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_debug(self): repl = ReplWindow( - study = self.model, - config = self.conf, - parent = self + study=self._study, + config=self.conf, + parent=self ) repl.show() def open_sqlite(self): - if self.model is None: + if self._study is None: logger.debug("No study open for sql debuging...") return - file = self.model.filename + file = self._study.filename _ = subprocess.Popen( f"sqlitebrowser {file}", shell=True diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index f7b5acd0..a045a1fa 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -115,6 +115,7 @@ class NodeItem(QGraphicsItem): if not self.graph._only_display: super(NodeItem, self).mouseMoveEvent(event) + class EdgeItem(QGraphicsItem): Type = QGraphicsItem.UserType + 2 @@ -177,12 +178,15 @@ class EdgeItem(QGraphicsItem): if self.graph.selected_item() == self: color = Qt.red elif self.graph.current_edge() == self: - color = Qt.black + color = Qt.blue elif not self.graph.graph.is_enable_edge(self.edge): color = Qt.darkGray - painter.setPen(QPen(color, 2, Qt.SolidLine, Qt.RoundCap, - Qt.RoundJoin)) + painter.setPen( + QPen( + color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin + ) + ) # Draw the line painter.drawLine(line) @@ -216,6 +220,7 @@ class EdgeItem(QGraphicsItem): painter.drawPolygon(poly) painter.fillPath(path, brush) + class NodeText(QGraphicsTextItem): def __init__(self, node_item): super(NodeText, self).__init__() @@ -228,9 +233,9 @@ class NodeText(QGraphicsTextItem): def set_custom_pos(self, pos): x = pos.x() - y = pos.y() - 42 # Dont panic! The answer is 42 + y = pos.y() - 42 # Dont panic! The answer is 42 - self.setPos(QPointF(x,y)) + self.setPos(QPointF(x, y)) @timer def paint(self, painter, option, widget): @@ -248,6 +253,7 @@ class NodeText(QGraphicsTextItem): # Update the node text self.setPlainText(self.item.node.name) + class NewEdgeLine(QGraphicsItem): def __init__(self, src, dest): super(NewEdgeLine, self).__init__() @@ -278,13 +284,14 @@ class NewEdgeLine(QGraphicsItem): Qt.RoundJoin)) painter.drawLine(line) + class GraphWidget(QGraphicsView): changeEdge = pyqtSignal(object) changeNode = pyqtSignal(object) def __init__(self, graph, parent=None, min_size=(400, 400), max_size=None, - size=None, only_display=False, undo = None): + size=None, only_display=False, undo=None): super(GraphWidget, self).__init__(parent=parent) self.timerId = 0 @@ -438,7 +445,7 @@ class GraphWidget(QGraphicsView): Nothing """ for i in self.texts: - if type(i) == NodeItem: + if type(i) is NodeItem: self.texts[i].rename() def enable_edge(self, edge, prev): @@ -522,7 +529,6 @@ class GraphWidget(QGraphicsView): self.changeNode.emit(self.sender()) self.changeEdge.emit(self.sender()) - def update_edges(self, node): """Update display of all edges linked with the node @@ -696,11 +702,11 @@ class GraphWidget(QGraphicsView): self._selected_new_edge_src_node = None items = self.items(event.pos()) - if items and type(items[0]) == EdgeItem: + if items and type(items[0]) is EdgeItem: edge = items[0] if edge: self.set_current_edge(edge) - elif items and type(items[0]) == NodeItem: + elif items and type(items[0]) is NodeItem: self._mouse_origin_x = pos.x() self._mouse_origin_y = pos.y() self._current_moved_node = items[0] @@ -708,7 +714,7 @@ class GraphWidget(QGraphicsView): # Add nodes and edges elif self._state == "add": items = self.items(event.pos()) - nodes = list(filter(lambda i: type(i) == NodeItem, items)) + nodes = list(filter(lambda i: type(i) is NodeItem, items)) if not nodes: self.add_node(pos) else: @@ -722,15 +728,15 @@ class GraphWidget(QGraphicsView): self._selected_new_edge_src_node = None items = list( filter( - lambda i: type(i) == NodeItem or type(i) == EdgeItem, + lambda i: type(i) is NodeItem or type(i) is EdgeItem, self.items(event.pos()) ) ) if len(items) > 0: item = items[0] - if type(item) == NodeItem: + if type(item) is NodeItem: self.del_node(item) - elif type(item) == EdgeItem: + elif type(item) is EdgeItem: self.del_edge(item) self.update() @@ -764,15 +770,14 @@ class GraphWidget(QGraphicsView): items = self.items(event.pos()) selectable_items = list( filter( - lambda i: (type(i) == NodeItem or - type(i) == EdgeItem), + lambda i: (type(i) is NodeItem or type(i) is EdgeItem), items ) ) if selectable_items: self.set_selected_item(selectable_items[0]) - elif self.clicked == False: + elif not self.clicked: self.set_selected_item(None) # Update temporary line @@ -791,7 +796,7 @@ class GraphWidget(QGraphicsView): if self._state == "move": # Move on scene if (not self.selected_item() and - event.buttons() & Qt.LeftButton): + event.buttons() & Qt.LeftButton): old_p = self.mapToScene(self.m_origin_x, self.m_origin_y) new_p = self.mapToScene(event.pos()) translation = new_p - old_p @@ -805,7 +810,6 @@ class GraphWidget(QGraphicsView): self.update() super(GraphWidget, self).mouseMoveEvent(event) - # Contextual menu def _menu_default(self, event, pos, items, menu): @@ -835,7 +839,8 @@ class GraphWidget(QGraphicsView): enable = menu.addAction(_translate("Network", "Enable the reach")) is_enable = False - reverse = menu.addAction(_translate("Network", "Reverse the reach orientation")) + reverse = menu.addAction(_translate( + "Network", "Reverse the reach orientation")) action = menu.exec_(self.mapToGlobal(event.pos())) @@ -856,7 +861,7 @@ class GraphWidget(QGraphicsView): menu = QMenu(self) if len(items) == 0: self._menu_default(event, pos, items, menu) - elif type(items[0]) == NodeItem: + elif type(items[0]) is NodeItem: self._menu_node(event, pos, items, menu) - elif type(items[0]) == EdgeItem: + elif type(items[0]) is EdgeItem: self._menu_edge(event, pos, items, menu) diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py index 733a910d..610235ba 100644 --- a/src/View/Network/Table.py +++ b/src/View/Network/Table.py @@ -22,7 +22,6 @@ import traceback from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph -from View.ASubWindow import ASubWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * from View.Tools.PamhyrTable import PamhyrTableModel @@ -38,6 +37,8 @@ from PyQt5.QtWidgets import ( QApplication, ) +logger = logging.getLogger() + class ComboBoxDelegate(QItemDelegate): def __init__(self, graph=None, parent=None): @@ -62,14 +63,16 @@ class ComboBoxDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() def currentItemChanged(self): self.commitData.emit(self.sender()) + class NodeTableModel(PamhyrTableModel): def _setup_lst(self): self._lst = self._data.nodes() @@ -121,6 +124,7 @@ class NodeTableModel(PamhyrTableModel): self._lst = self._data.nodes() self.layoutChanged.emit() + class EdgeTableModel(PamhyrTableModel): def _setup_lst(self): self._lst = self._data.edges() @@ -141,8 +145,8 @@ class EdgeTableModel(PamhyrTableModel): try: if (self._headers[index.column()] == "node1" or - self._headers[index.column()] == "node2"): - node = self.graph.node(value) + self._headers[index.column()] == "node2"): + node = self._data.node(value) self._undo.push( SetNodeCommand( self._data, diff --git a/src/View/Network/UndoCommand.py b/src/View/Network/UndoCommand.py index c9cce895..1c1d9da1 100644 --- a/src/View/Network/UndoCommand.py +++ b/src/View/Network/UndoCommand.py @@ -31,6 +31,7 @@ from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph + class AddNodeCommand(QUndoCommand): def __init__(self, graph, node): QUndoCommand.__init__(self) @@ -44,6 +45,7 @@ class AddNodeCommand(QUndoCommand): def redo(self): self._graph.insert_node(self._node) + class SetNodePosCommand(QUndoCommand): def __init__(self, node, new, old): QUndoCommand.__init__(self) @@ -58,6 +60,7 @@ class SetNodePosCommand(QUndoCommand): def redo(self): self._node.node.setPos(self._new[0], self._new[1]) + class DelNodeCommand(QUndoCommand): def __init__(self, graph, node): QUndoCommand.__init__(self) @@ -86,6 +89,7 @@ class DelNodeCommand(QUndoCommand): self._graph.remove_node(self._node.name) + class AddEdgeCommand(QUndoCommand): def __init__(self, graph, edge): QUndoCommand.__init__(self) @@ -99,6 +103,7 @@ class AddEdgeCommand(QUndoCommand): def redo(self): self._graph.insert_edge(self._edge) + class DelEdgeCommand(QUndoCommand): def __init__(self, graph, edge): QUndoCommand.__init__(self) @@ -144,6 +149,7 @@ class SetNodeCommand(QUndoCommand): def redo(self): self._el[self._column] = self._new + class EnableEdgeCommand(QUndoCommand): def __init__(self, edge, enable): QUndoCommand.__init__(self) @@ -153,10 +159,11 @@ class EnableEdgeCommand(QUndoCommand): self._enable = enable def undo(self): - self._edge.enable(enable = self._old) + self._edge.enable(enable=self._old) def redo(self): - self._edge.enable(enable = self._enable) + self._edge.enable(enable=self._enable) + class ReverseEdgeCommand(QUndoCommand): def __init__(self, edge): diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 95e575ea..1f8bb811 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +import logging + from PyQt5.QtCore import QCoreApplication from PyQt5.QtGui import ( @@ -36,58 +38,50 @@ from PyQt5.QtWidgets import ( from Model.River import RiverNode, RiverReach, River -from View.ASubWindow import ASubMainWindow +from View.Tools.PamhyrWindow import PamhyrWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * -from View.Network.translate import ( - table_headers_node, table_headers_edge, - retranslate, -) +from View.Network.translate import NetworkTranslate from View.Network.Table import ( ComboBoxDelegate, NodeTableModel, EdgeTableModel, ) +logger = logging.getLogger() + _translate = QCoreApplication.translate -class NetworkWindow(ASubMainWindow): - def __init__(self, model=None, title="River network", parent=None): - self._title = title - self._model = model - self._graph = self._model.river - - self.setup_title() +class NetworkWindow(PamhyrWindow): + _pamhyr_ui = "Network" + _pamhyr_name = "River network" + def __init__(self, study=None, config=None, parent=None): super(NetworkWindow, self).__init__( - name=title, ui="Network", parent=parent + title=self._pamhyr_name + " - " + study.name, + study=study, + config=config, + trad=NetworkTranslate(), + options=['undo'], + parent=parent, ) - self.ui.setWindowTitle(self._title) - self.setup_sc() + self._graph = study.river + self._table_headers_node = self._trad.get_dict("table_headers_node") + self._table_headers_edge = self._trad.get_dict("table_headers_edge") + self.setup_graph() self.setup_table() self.setup_connections() - def setup_title(self): - self._title = self._title + " - " + self._model.name - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - def setup_table(self): - retranslate() - # Nodes table table = self.find(QTableView, "tableView_nodes") self._nodes_model = NodeTableModel( - table_view = table, - table_headers = table_headers_node, - editable_headers = ["name"], - data = self._graph, - undo = self._undo_stack, + table_view=table, + table_headers=self._table_headers_node, + editable_headers=["name"], + data=self._graph, + undo=self._undo_stack, ) table.setModel(self._nodes_model) @@ -97,30 +91,30 @@ class NetworkWindow(ASubMainWindow): # Edges table table = self.find(QTableView, "tableView_reachs") self.delegate_combobox = ComboBoxDelegate( - graph = self._graph, - parent = self, + graph=self._graph, + parent=self, ) self._reachs_model = EdgeTableModel( - table_view = table, - table_headers = table_headers_edge, - editable_headers = ["name", "node1", "node2"], - delegates = { + table_view=table, + table_headers=self._table_headers_edge, + editable_headers=["name", "node1", "node2"], + delegates={ "node1": self.delegate_combobox, "node2": self.delegate_combobox, }, - data = self._graph, - undo = self._undo_stack, + data=self._graph, + undo=self._undo_stack, ) table.setModel(self._reachs_model) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - #table.resizeColumnsToContents() + # table.resizeColumnsToContents() def setup_graph(self): self._graph_widget = GraphWidget( self._graph, parent=self, - undo = self._undo_stack + undo=self._undo_stack ) self._graph_layout = self.find(QHBoxLayout, "horizontalLayout_graph") self._graph_layout.addWidget(self._graph_widget) @@ -128,7 +122,8 @@ class NetworkWindow(ASubMainWindow): def setup_connections(self): self._nodes_model.dataChanged.connect(self._reachs_model.update) self._nodes_model.dataChanged.connect(self._graph_widget.rename_nodes) - self._reachs_model.dataChanged.connect(self._graph_widget.display_update) + self._reachs_model.dataChanged.connect( + self._graph_widget.display_update) self._reachs_model.dataChanged.connect(self._nodes_model.update) self._graph_widget.changeEdge.connect(self._reachs_model.update) self._graph_widget.changeNode.connect(self._nodes_model.update) @@ -143,9 +138,6 @@ class NetworkWindow(ASubMainWindow): self.clicked_del ) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - def clicked_add(self): if self.get_action_checkable("action_toolBar_add"): self.set_action_checkable("action_toolBar_del", False) @@ -166,13 +158,15 @@ class NetworkWindow(ASubMainWindow): if key == Qt.Key_Escape: self._graph_widget.reset_selection - def undo(self): + # Redefine undo/redo method + + def _undo(self): self._undo_stack.undo() self._reachs_model.update() self._nodes_model.update() self._graph_widget.display_update() - def redo(self): + def _redo(self): self._undo_stack.redo() self._reachs_model.update() self._nodes_model.update() diff --git a/src/View/Network/translate.py b/src/View/Network/translate.py index 24935e1b..ac8431ae 100644 --- a/src/View/Network/translate.py +++ b/src/View/Network/translate.py @@ -18,23 +18,22 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers_node = { - "name": _translate("Network", "Name"), - "type": _translate("Network", "Type"), -} -table_headers_edge = { - "name": _translate("Network", "Name"), - "node1": _translate("Network", "Source node"), - "node2": _translate("Network", "Destination node"), -} +class NetworkTranslate(PamhyrTranslate): + def __init__(self): + super(NetworkTranslate, self).__init__() -def retranslate(): - table_headers_node["name"] = _translate("Network", "Name") - table_headers_node["type"] = _translate("Network", "Type") + self._sub_dict["table_headers_node"] = { + "name": _translate("Network", "Name"), + "type": _translate("Network", "Type"), + } - table_headers_edge["name"] = _translate("Network", "Name") - table_headers_edge["node1"] = _translate("Network", "Source node") - table_headers_edge["node2"] = _translate("Network", "Destination node") + self._sub_dict["table_headers_edge"] = { + "name": _translate("Network", "Name"), + "node1": _translate("Network", "Source node"), + "node2": _translate("Network", "Destination node"), + } diff --git a/src/View/Reservoir/Edit/Plot.py b/src/View/Reservoir/Edit/Plot.py new file mode 100644 index 00000000..8c75a199 --- /dev/null +++ b/src/View/Reservoir/Edit/Plot.py @@ -0,0 +1,101 @@ +# Plot.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from datetime import datetime + +from tools import timer, trace +from View.Tools.PamhyrPlot import PamhyrPlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +from View.BoundaryCondition.Edit.translate import BCETranslate + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class Plot(PamhyrPlot): + def __init__(self, mode="time", data=None, + trad=None, canvas=None, toolbar=None, + parent=None): + super(Plot, self).__init__( + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent + ) + + self._table_headers = self._trad.get_dict("table_headers") + self._mode = mode + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if len(self.data) == 0: + self._init = False + return + + # Plot data + x = list(map(lambda v: v[0], self.data.data)) + y = list(map(lambda v: v[1], self.data.data)) + self._line, = self.canvas.axes.plot( + x, y, + color='r', lw=1., + markersize=5, marker='+', + picker=30, + ) + + # Plot label + # header = self.data.header + self.canvas.axes.set_xlabel( + self._table_headers["z"], color='black', fontsize=10 + ) + self.canvas.axes.set_ylabel( + self._table_headers["Area"], color='black', fontsize=10 + ) + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + # self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if not self._init: + self.draw() + return + + x = list(map(lambda v: v[0], self.data.data)) + y = list(map(lambda v: v[1], self.data.data)) + + self._line.set_data(x, y) + + self.canvas.axes.relim() + self.canvas.axes.autoscale() + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Reservoir/Edit/Table.py b/src/View/Reservoir/Edit/Table.py new file mode 100644 index 00000000..21e0f91b --- /dev/null +++ b/src/View/Reservoir/Edit/Table.py @@ -0,0 +1,141 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from datetime import date, time, datetime, timedelta + +from tools import trace, timer + +from View.Tools.PamhyrTable import PamhyrTableModel + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, QTime, QDateTime, +) + +from PyQt5.QtWidgets import ( + QTableView, QAbstractItemView, QSpinBox, QItemDelegate, +) + +from View.Reservoir.Edit.UndoCommand import ( + SetDataCommand, AddCommand, DelCommand, + SortCommand, PasteCommand, +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class TableModel(PamhyrTableModel): + def data(self, index, role): + if role == Qt.TextAlignmentRole: + return Qt.AlignHCenter | Qt.AlignVCenter + + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + value = QVariant() + + if 0 <= column < 2: + v = self._data.get_i(row)[column] + value = f"{v:.4f}" + + return value + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + try: + self._undo.push( + SetDataCommand( + self._data, row, column, float(value) + ) + ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._data, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._data, rows + ) + ) + + self.endRemoveRows() + + def sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo.push( + SortCommand( + self._data, _reverse + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() + + def paste(self, row, data): + if len(data) == 0: + return + + self.layoutAboutToBeChanged.emit() + + self._undo.push( + PasteCommand( + self._data, row, + list( + map( + lambda d: self._data.new_from_data(d), + data + ) + ) + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() diff --git a/src/View/Reservoir/Edit/Translate.py b/src/View/Reservoir/Edit/Translate.py new file mode 100644 index 00000000..f7660885 --- /dev/null +++ b/src/View/Reservoir/Edit/Translate.py @@ -0,0 +1,35 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +from View.Reservoir.Translate import ReservoirTranslate + +_translate = QCoreApplication.translate + + +class EditReservoirTranslate(ReservoirTranslate): + def __init__(self): + super(EditReservoirTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "z": _translate("Reservoir", "Elevation (m)"), + "Area": _translate("Reservoir", "Area (hectare)"), + } diff --git a/src/View/Reservoir/Edit/UndoCommand.py b/src/View/Reservoir/Edit/UndoCommand.py new file mode 100644 index 00000000..22b7a459 --- /dev/null +++ b/src/View/Reservoir/Edit/UndoCommand.py @@ -0,0 +1,135 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Reservoir.Reservoir import Reservoir + +logger = logging.getLogger() + + +class SetDataCommand(QUndoCommand): + def __init__(self, data, index, column, new_value): + QUndoCommand.__init__(self) + + self._data = data + self._index = index + self._column = column + self._old = self._data.get_i(self._index)[self._column] + self._new = new_value + + def undo(self): + self._data._set_i_c_v(self._index, self._column, self._old) + + def redo(self): + self._data._set_i_c_v(self._index, self._column, self._new) + + +class AddCommand(QUndoCommand): + def __init__(self, data, index): + QUndoCommand.__init__(self) + + self._data = data + self._index = index + self._new = None + + def undo(self): + self._data.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._data.add(self._index) + else: + self._data.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, data, rows): + QUndoCommand.__init__(self) + + self._data = data + self._rows = rows + + self._point = [] + for row in rows: + self._point.append((row, self._data.get_i(row))) + self._point.sort() + + def undo(self): + for row, el in self._point: + self._data.insert(row, el) + + def redo(self): + self._data.delete_i(self._rows) + + +class SortCommand(QUndoCommand): + def __init__(self, data, _reverse): + QUndoCommand.__init__(self) + + self._data = data + self._reverse = _reverse + + self._old = self._data.data + self._indexes = None + + def undo(self): + ll = self._data.data + self._data.sort( + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._data.sort( + _reverse=self._reverse, + key=lambda x: x[0] + ) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._data.data + ) + ) + self._old = None + + +class PasteCommand(QUndoCommand): + def __init__(self, data, row, hs): + QUndoCommand.__init__(self) + + self._data = data + self._row = row + self._h = hs + self._h.reverse() + + def undo(self): + self._data.delete_i( + range(self._row, self._row + len(self._h)) + ) + + def redo(self): + for h in self._h: + self._data.insert(self._row, h) diff --git a/src/View/Reservoir/Edit/Window.py b/src/View/Reservoir/Edit/Window.py new file mode 100644 index 00000000..865ae6c8 --- /dev/null +++ b/src/View/Reservoir/Edit/Window.py @@ -0,0 +1,212 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.PamhyrWidget import PamhyrWidget + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.Reservoir.Edit.Translate import EditReservoirTranslate +from View.Reservoir.Edit.Table import TableModel +from View.Reservoir.Edit.Plot import Plot + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class EditReservoirWindow(PamhyrWindow): + _pamhyr_ui = "Reservoir" + _pamhyr_name = "Edit Reservoir" + + def __init__(self, data=None, study=None, config=None, parent=None): + self._data = data + trad = EditReservoirTranslate() + + name = self._pamhyr_name + if self._data is not None: + node_name = (self._data.node.name if self._data.node is not None + else _translate("Reservoir", "Not associated")) + name = ( + _translate("Edit Reservoir", self._pamhyr_name) + + f" - {study.name} " + + f" - {self._data.name} ({self._data.id}) " + + f"({node_name})" + ) + + super(EditReservoirWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._hash_data.append(data) + + self.setup_table() + self.setup_plot() + self.setup_connections() + + def setup_table(self): + headers = {} + table_headers = self._trad.get_dict("table_headers") + + table = self.find(QTableView, "tableView") + self._table = TableModel( + table_view=table, + table_headers=table_headers, + editable_headers=table_headers, + delegates={}, + data=self._data, + undo=self._undo_stack, + opt_data=self._study.time_system + ) + + table.setModel(self._table) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_plot(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.verticalLayout.addWidget(self.toolbar) + self.verticalLayout.addWidget(self.canvas) + + self.plot = Plot( + canvas=self.canvas, + data=self._data, + mode=self._study.time_system, + trad=self._trad, + toolbar=self.toolbar, + ) + self.plot.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self.find(QAction, "action_sort").triggered.connect(self.sort) + + self._table.dataChanged.connect(self.update) + + def update(self): + self.plot.update() + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + return table.selectionModel()\ + .selectedRows()[0]\ + .row() + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._data) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + self.plot.update() + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + self.plot.update() + + def sort(self): + self._table.sort(False) + self.plot.update() + + def _copy(self): + rows = self.index_selected_rows() + + table = [] + # table.append(self._data.header) + table.append(self._trad.get_dict("table_headers")) + + data = self._data.data + for row in rows: + table.append(list(data[row])) + + self.copyTableIntoClipboard(table) + + def _paste(self): + header, data = self.parseClipboardTable() + + logger.debug(f"paste: h:{header}, d:{data}") + + if len(data) == 0: + return + + row = 0 + rows = self.index_selected_rows() + if len(rows) != 0: + row = rows[0] + + self._table.paste(row, data) + self.plot.update() + + def _undo(self): + self._table.undo() + self.plot.update() + self.widget_update() + + def _redo(self): + self._table.redo() + self.plot.update() + self.widget_update() diff --git a/src/View/Reservoir/Table.py b/src/View/Reservoir/Table.py new file mode 100644 index 00000000..14362050 --- /dev/null +++ b/src/View/Reservoir/Table.py @@ -0,0 +1,183 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.Reservoir.UndoCommand import ( + SetNameCommand, SetNodeCommand, + AddCommand, DelCommand, PasteCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + nodes = list( + map( + lambda n: n.name, + filter( + lambda n: not ( + self._data.is_upstream_node(n) + or self._data.is_downstream_node(n) + ), + self._data.nodes() + ) + ) + ) + + self.editor.addItems( + [_translate("Reservoir", "Not associated")] + + nodes + ) + + self.editor.setCurrentText(index.data(Qt.DisplayRole)) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data.reservoir + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._lst.get(row).name + elif self._headers[column] == "node": + n = self._lst.get(row).node + if n is None: + return _translate("Reservoir", "Not associated") + return n.name + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._lst, row, value + ) + ) + elif self._headers[column] == "node": + self._undo.push( + SetNodeCommand( + self._lst, row, self._data.node(value) + ) + ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._lst, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._lst, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/Reservoir/Translate.py b/src/View/Reservoir/Translate.py new file mode 100644 index 00000000..556c9381 --- /dev/null +++ b/src/View/Reservoir/Translate.py @@ -0,0 +1,33 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class ReservoirTranslate(PamhyrTranslate): + def __init__(self): + super(ReservoirTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("Reservoir", "Name"), + "node": _translate("Reservoir", "Node") + } diff --git a/src/View/Reservoir/UndoCommand.py b/src/View/Reservoir/UndoCommand.py new file mode 100644 index 00000000..078c31d3 --- /dev/null +++ b/src/View/Reservoir/UndoCommand.py @@ -0,0 +1,127 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Reservoir.Reservoir import Reservoir +from Model.Reservoir.ReservoirList import ReservoirList + + +class SetNameCommand(QUndoCommand): + def __init__(self, reservoir_lst, index, new_value): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + self._index = index + self._old = self._reservoir_lst.get(self._index).name + self._new = str(new_value) + + def undo(self): + self._reservoir_lst.get(self._index).name = self._old + + def redo(self): + self._reservoir_lst.get(self._index).name = self._new + + +class SetNodeCommand(QUndoCommand): + def __init__(self, reservoir_lst, index, node): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + self._index = index + self._old = self._reservoir_lst.get(self._index).node + self._new = node + self._prev_assoc_to_node = self._reservoir_lst.get_assoc_to_node(node) + + def _previous_assoc_node(self, node): + if self._prev_assoc_to_node is not None: + self._prev_assoc_to_node.node = node + + def undo(self): + self._reservoir_lst.get(self._index).node = self._old + self._previous_assoc_node(self._new) + + def redo(self): + self._reservoir_lst.get(self._index).node = self._new + self._previous_assoc_node(None) + + +class AddCommand(QUndoCommand): + def __init__(self, reservoir_lst, index): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + + self._index = index + self._new = None + + def undo(self): + self._reservoir_lst.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._reservoir_lst.new(self._index) + else: + self._reservoir_lst.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, reservoir_lst, rows): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + + self._rows = rows + + self._reservoir = [] + for row in rows: + self._reservoir.append((row, self._reservoir_lst.get(row))) + self._reservoir.sort() + + def undo(self): + for row, el in self._reservoir: + self._reservoir_lst.insert(row, el) + + def redo(self): + self._reservoir_lst.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, reservoir_lst, row, reservoir): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + + self._row = row + self._reservoir = deepcopy(reservoir) + self._reservoir.reverse() + + def undo(self): + self._reservoir_lst.delete_i( + self._tab, + range(self._row, self._row + len(self._reservoir)) + ) + + def redo(self): + for r in self._reservoir: + self._reservoir_lst.insert(self._row, r) diff --git a/src/View/Reservoir/Window.py b/src/View/Reservoir/Window.py new file mode 100644 index 00000000..147c05d1 --- /dev/null +++ b/src/View/Reservoir/Window.py @@ -0,0 +1,173 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.Reservoir.Table import ( + TableModel, ComboBoxDelegate +) + +from View.Network.GraphWidget import GraphWidget +from View.Reservoir.Translate import ReservoirTranslate +from View.Reservoir.Edit.Window import EditReservoirWindow + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class ReservoirWindow(PamhyrWindow): + _pamhyr_ui = "ReservoirList" + _pamhyr_name = "Reservoir" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name + + super(ReservoirWindow, self).__init__( + title=name, + study=study, + config=config, + trad=ReservoirTranslate(), + parent=parent + ) + + self._reservoir_lst = self._study.river.reservoir + + self.setup_table() + self.setup_graph() + self.setup_connections() + + def setup_table(self): + self._table = None + + self._delegate_node = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "node"], + delegates={ + "node": self._delegate_node, + }, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + ) + + def setup_graph(self): + self.graph_widget = GraphWidget( + self._study.river, + min_size=None, size=(200, 200), + only_display=True, + parent=self + ) + self.graph_layout = self.find(QVBoxLayout, "verticalLayout") + self.graph_layout.addWidget(self.graph_widget) + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self.find(QAction, "action_edit").triggered.connect(self.edit) + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + return table.selectionModel()\ + .selectedRows()[0]\ + .row() + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._reservoir_lst) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + + def _copy(self): + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + self._table.undo() + + def _redo(self): + self._table.redo() + + def edit(self): + rows = self.index_selected_rows() + for row in rows: + data = self._reservoir_lst.get(row) + + if self.sub_window_exists( + EditReservoirWindow, + data=[self._study, None, data] + ): + continue + + win = EditReservoirWindow( + data=data, + study=self._study, + parent=self + ) + win.show() diff --git a/src/View/Results/CustomPlot/CustomPlotValuesSelectionDialog.py b/src/View/Results/CustomPlot/CustomPlotValuesSelectionDialog.py new file mode 100644 index 00000000..1ffd6732 --- /dev/null +++ b/src/View/Results/CustomPlot/CustomPlotValuesSelectionDialog.py @@ -0,0 +1,97 @@ +# CustomPlotValuesSelectionDialog.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from View.Tools.PamhyrWindow import PamhyrDialog + +from PyQt5.QtWidgets import ( + QRadioButton, QCheckBox, QVBoxLayout, +) + +from View.Results.CustomPlot.Translate import CustomPlotTranslate + + +class CustomPlotValuesSelectionDialog(PamhyrDialog): + _pamhyr_ui = "CustomPlotValuesSelectionDialog" + _pamhyr_name = "Custom Plot Selection" + + def __init__(self, parent=None): + super(CustomPlotValuesSelectionDialog, self).__init__( + title=self._pamhyr_name, + options=[], + trad=CustomPlotTranslate(), + parent=parent + ) + + self._available_values_x = self._trad.get_dict("values_x") + self._available_values_y = self._trad.get_dict("values_y") + + self.setup_radio_buttons() + self.setup_check_boxs() + + self.value = None + + def setup_radio_buttons(self): + self._radio = [] + layout = self.find(QVBoxLayout, "verticalLayout_x") + + for value in self._available_values_x: + btn = QRadioButton( + self._available_values_x[value], + parent=self + ) + self._radio.append((value, btn)) + layout.addWidget(btn) + + self._radio[0][1].setChecked(True) + layout.addStretch() + + def setup_check_boxs(self): + self._check = [] + layout = self.find(QVBoxLayout, "verticalLayout_y") + + for value in self._available_values_y: + btn = QCheckBox( + self._available_values_y[value], + parent=self + ) + self._check.append((value, btn)) + layout.addWidget(btn) + + self._check[0][1].setChecked(True) + layout.addStretch() + + def accept(self): + x = next( + filter( + lambda r: r[1].isChecked(), + self._radio + ) + )[0] + + y = list( + map( + lambda b: b[0], + filter( + lambda b: b[1].isChecked(), + self._check + ) + ) + ) + + self.value = x, y + super().accept() diff --git a/src/View/Results/CustomPlot/Plot.py b/src/View/Results/CustomPlot/Plot.py new file mode 100644 index 00000000..80dcf550 --- /dev/null +++ b/src/View/Results/CustomPlot/Plot.py @@ -0,0 +1,346 @@ +# Plot.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce +from datetime import datetime + +from tools import timer +from View.Tools.PamhyrPlot import PamhyrPlot + +from View.Results.CustomPlot.Translate import CustomPlotTranslate + +logger = logging.getLogger() + +unit = { + "elevation": "0-meter", + "water_elevation": "0-meter", + "discharge": "1-m3s", +} + + +class CustomPlot(PamhyrPlot): + def __init__(self, x, y, reach, profile, timestamp, + data=None, canvas=None, trad=None, + toolbar=None, parent=None): + super(CustomPlot, self).__init__( + canvas=canvas, + trad=CustomPlotTranslate(), + data=data, + toolbar=toolbar, + parent=parent + ) + + self._x = x + self._y = y + self._reach = reach + self._profile = profile + self._timestamp = timestamp + + logger.debug( + "Create custom plot for: " + + f"{x} -> {','.join(y)}: " + + f"reach={reach}, profile={profile}, " + + f"timestamp={timestamp}" + ) + + self._y_axes = sorted( + set( + map( + lambda y: unit[y], + self._y + ) + ) + ) + + self._axes = {} + + def _draw_kp(self): + results = self.data + reach = results.river.reach(self._reach) + kp = reach.geometry.get_kp() + z_min = reach.geometry.get_z_min() + + self.canvas.axes.set_xlim( + left=min(kp), right=max(kp) + ) + + meter_axes = self.canvas.axes + m3S_axes = self.canvas.axes + if "0-meter" in self._y_axes and "1-m3s" in self._y_axes: + m3s_axes = self._axes["1-m3s"] + + lines = {} + if "elevation" in self._y: + meter_axes.set_ylim( + bottom=min(0, min(z_min)), + top=max(z_min) + 1 + ) + + line = meter_axes.plot( + kp, z_min, + color='grey', lw=1., + ) + lines["elevation"] = line + + if "water_elevation" in self._y: + # Water elevation + water_z = list( + map( + lambda p: p.get_ts_key(self._timestamp, "Z"), + reach.profiles + ) + ) + + meter_axes.set_ylim( + bottom=min(0, min(z_min)), + top=max(water_z) + 1 + ) + + line = meter_axes.plot( + kp, water_z, lw=1., + color='blue', + ) + lines["water_elevation"] = line + + if "elevation" in self._y: + meter_axes.fill_between( + kp, z_min, water_z, + color='blue', alpha=0.5, interpolate=True + ) + + if "discharge" in self._y: + q = list( + map( + lambda p: p.get_ts_key(self._timestamp, "Q"), + reach.profiles + ) + ) + + m3s_axes.set_ylim( + bottom=min(0, min(q)), + top=max(q) + 1 + ) + + line = m3s_axes.plot( + kp, q, lw=1., + color='r', + ) + lines["discharge"] = line + + # Legend + lns = reduce( + lambda acc, line: acc + line, + map(lambda line: lines[line], lines), + [] + ) + labs = list(map(lambda line: self._trad[line], lines)) + self.canvas.axes.legend(lns, labs, loc="lower left") + + def _customize_x_axes_time(self, ts, mode="time"): + # Custom time display + nb = len(ts) + mod = int(nb / 5) + mod = mod if mod > 0 else nb + + fx = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] % mod == 0, + enumerate(ts) + ) + ) + ) + + if mode == "time": + t0 = datetime.fromtimestamp(0) + xt = list( + map( + lambda v: ( + str( + datetime.fromtimestamp(v) - t0 + ).split(",")[0] + .replace("days", self._trad["days"]) + .replace("day", self._trad["day"]) + ), + fx + ) + ) + else: + xt = list( + map( + lambda v: str(datetime.fromtimestamp(v).date()), + fx + ) + ) + + self.canvas.axes.set_xticks(ticks=fx, labels=xt, rotation=45) + + def _draw_time(self): + results = self.data + reach = results.river.reach(self._reach) + profile = reach.profile(self._profile) + + meter_axes = self.canvas.axes + m3S_axes = self.canvas.axes + if "0-meter" in self._y_axes and "1-m3s" in self._y_axes: + m3s_axes = self._axes["1-m3s"] + + ts = list(results.get("timestamps")) + ts.sort() + + self.canvas.axes.set_xlim( + left=min(ts), right=max(ts) + ) + + x = ts + lines = {} + if "elevation" in self._y: + # Z min is constant in time + z_min = profile.geometry.z_min() + ts_z_min = list( + map( + lambda ts: z_min, + ts + ) + ) + + line = meter_axes.plot( + ts, ts_z_min, + color='grey', lw=1. + ) + lines["elevation"] = line + + if "water_elevation" in self._y: + # Water elevation + z = profile.get_key("Z") + + meter_axes.set_ylim( + bottom=min(0, min(z)), + top=max(z) + 1 + ) + + line = meter_axes.plot( + ts, z, lw=1., + color='b', + ) + lines["water_elevation"] = line + + if "elevation" in self._y: + z_min = profile.geometry.z_min() + ts_z_min = list( + map( + lambda ts: z_min, + ts + ) + ) + + meter_axes.fill_between( + ts, ts_z_min, z, + color='blue', alpha=0.5, interpolate=True + ) + + if "discharge" in self._y: + q = profile.get_key("Q") + + m3s_axes.set_ylim( + bottom=min(0, min(q)), + top=max(q) + 1 + ) + + line = m3s_axes.plot( + ts, q, lw=1., + color='r', + ) + lines["discharge"] = line + + self._customize_x_axes_time(ts) + + # Legend + lns = reduce( + lambda acc, line: acc + line, + map(lambda line: lines[line], lines), + [] + ) + labs = list(map(lambda line: self._trad[line], lines)) + self.canvas.axes.legend(lns, labs, loc="lower left") + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + self.canvas.axes.set_xlabel( + self._trad[self._x], + color='black', fontsize=10 + ) + + self.canvas.axes.set_ylabel( + self._trad[self._y_axes[0]], + color='black', fontsize=10 + ) + + for axes in self._y_axes[1:]: + if axes in self._axes: + continue + + ax_new = self.canvas.axes.twinx() + ax_new.set_ylabel( + self._trad[axes], + color='black', fontsize=10 + ) + self._axes[axes] = ax_new + + if self._x == "kp": + self._draw_kp() + elif self._x == "time": + self._draw_time() + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + @timer + def update(self): + if not self._init: + self.draw() + return + + def set_reach(self, reach_id): + self._reach = reach_id + self._profile = 0 + + self.update() + + def set_profile(self, profile_id): + self._profile = profile_id + + if self._x != "kp": + self.update() + + def set_timestamp(self, timestamp): + self._timestamp = timestamp + + if self._x != "time": + self.update() diff --git a/src/View/Results/CustomPlot/Translate.py b/src/View/Results/CustomPlot/Translate.py new file mode 100644 index 00000000..555950cd --- /dev/null +++ b/src/View/Results/CustomPlot/Translate.py @@ -0,0 +1,67 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Results.translate import ResultsTranslate + +_translate = QCoreApplication.translate + + +class CustomPlotTranslate(ResultsTranslate): + def __init__(self): + super(CustomPlotTranslate, self).__init__() + + # Value type + + self._dict['time'] = _translate( + "CustomPlot", "Time (sec)" + ) + self._dict['kp'] = _translate( + "CustomPlot", "Kp (m)" + ) + self._dict['elevation'] = _translate( + "CustomPlot", "Elevation (m)" + ) + self._dict['water_elevation'] = _translate( + "CustomPlot", "Water elevation (m)" + ) + self._dict['discharge'] = _translate( + "CustomPlot", "Discharge (m³/s)" + ) + + # Unit corresponding long name (plot axes display) + + self._dict['0-meter'] = _translate( + "CustomPlot", "Elevation (m)" + ) + self._dict['1-m3s'] = _translate( + "CustomPlot", "Discharge (m³/s)" + ) + + # SubDict + + self._sub_dict["values_x"] = { + "kp": self._dict["kp"], + "time": self._dict["time"], + } + self._sub_dict["values_y"] = { + "elevation": self._dict["elevation"], + "water_elevation": self._dict["water_elevation"], + "discharge": self._dict["discharge"], + } diff --git a/src/View/Results/PlotAC.py b/src/View/Results/PlotAC.py index f0e4b0e2..8f98973d 100644 --- a/src/View/Results/PlotAC.py +++ b/src/View/Results/PlotAC.py @@ -17,7 +17,8 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot +from matplotlib import pyplot as plt from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +26,17 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotAC(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): + +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotAC, self).__init__( canvas=canvas, + trad=trad, data=results, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self._current_timestamp = max(results.get("timestamps")) @@ -43,6 +47,11 @@ class PlotAC(APlot): def results(self): return self.data + @results.setter + def results(self, results): + self.data = results + self._current_timestamp = max(results.get("timestamps")) + @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -53,11 +62,11 @@ class PlotAC(APlot): self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "X (m)"), - color='green', fontsize=11 + color='black', fontsize=11 ) self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Elevation (m)"), - color='green', fontsize=11 + color='black', fontsize=11 ) reach = self.results.river.reach(self._current_reach_id) @@ -66,7 +75,7 @@ class PlotAC(APlot): z = profile.geometry.z() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) self.line_kp, = self.canvas.axes.plot( @@ -81,17 +90,17 @@ class PlotAC(APlot): # Water elevation water_z = profile.get_ts_key(self._current_timestamp, "Z") - self.canvas.axes.plot( + self.water, = self.canvas.axes.plot( [min(x), max(x)], [water_z, water_z], lw=1., color='b', ) - x_z = list(map(lambda _: water_z, x)) - self.canvas.axes.fill_between( + self.collection = self.canvas.axes.fill_between( x, z, water_z, where=z <= water_z, - color='blue', alpha=0.5, interpolate=True + color='skyblue', alpha=0.7, interpolate=True ) + self.liste_chemins = self.collection.get_paths() self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() @@ -101,15 +110,60 @@ class PlotAC(APlot): def set_reach(self, reach_id): self._current_reach_id = reach_id self._current_profile_id = 0 - self.draw() + self.update() def set_profile(self, profile_id): self._current_profile_id = profile_id - self.draw() + self.update() def set_timestamp(self, timestamp): self._current_timestamp = timestamp - self.draw() + self.update_poly() def update(self): - self.draw() + + reach = self.results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id) + x = profile.geometry.get_station() + z = profile.geometry.z() + + self.line_kp.set_data(x, z) + + self.canvas.axes.set_xlim( + left=min(x), right=max(x) + ) + + kp = reach.geometry.get_kp() + + # Water elevation + water_z = profile.get_ts_key(self._current_timestamp, "Z") + + self.water.set_data([min(x), max(x)], [water_z, water_z]) + + self.collection.remove() + self.collection = self.canvas.axes.fill_between( + x, z, water_z, + where=z <= water_z, + color='skyblue', alpha=0.7, interpolate=True + ) + self.canvas.figure.canvas.draw_idle() + + def update_poly(self): + + reach = self.results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id) + x = profile.geometry.get_station() + z = profile.geometry.z() + + # Water elevation + water_z = profile.get_ts_key(self._current_timestamp, "Z") + + self.water.set_data([min(x), max(x)], [water_z, water_z]) + + self.collection.remove() + self.collection = self.canvas.axes.fill_between( + x, z, water_z, + where=z <= water_z, + color='skyblue', alpha=0.7, interpolate=True + ) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index 557c7cec..f236bf8f 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -22,7 +22,7 @@ from functools import reduce from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -32,14 +32,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotH(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): + +class PlotH(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotH, self).__init__( canvas=canvas, + trad=trad, data=results, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self._mode = "time" @@ -52,6 +55,11 @@ class PlotH(APlot): def results(self): return self.data + @results.setter + def results(self, results): + self.data = results + self._current_timestamp = max(results.get("timestamps")) + @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -74,43 +82,34 @@ class PlotH(APlot): # Axes self.canvas.axes.set_xlabel( _translate("Results", "Time (s)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( _translate("Results", "Discharge (m³/s)"), - color='green', fontsize=12 + color='black', fontsize=10 ) - ts = list(self.results.get("timestamps")) - ts.sort() - - self.canvas.axes.set_xlim( - left = min(ts), right = max(ts) - ) + self.ts = list(self.results.get("timestamps")) + self.ts.sort() # Draw discharge for each timestamp - x = ts + x = self.ts y = profile.get_key("Q") - if len(ts) != len(x): - logger.warning( - "Results as less Q data ({len(x)}) " + - "than timestamps ({len(ts)}) " + - "for profile {self._current_profile_id}" - ) - return - - self.canvas.axes.set_ylim( - [min(min(y),0), max(y) + 10] + self._line, = self.canvas.axes.plot( + x, y, lw=1., + color='r', + markersize=3, marker='+' ) - self._line = [ - self.canvas.axes.plot( - x, y, lw=1., - color='r', - markersize=3, marker='+' - ) - ] + self._current, = self.canvas.axes.plot( + self._current_timestamp, + y[self.ts.index(self._current_timestamp)], + lw=1., color='b', + markersize=3, marker='+' + ) + + self.canvas.axes.relim() # Custom time display nb = len(x) @@ -134,8 +133,8 @@ class PlotH(APlot): lambda v: ( str( datetime.fromtimestamp(v) - t0 - ).split(",")[0]\ - .replace("days", _translate("Results", "days"))\ + ).split(",")[0] + .replace("days", _translate("Results", "days")) .replace("day", _translate("Results", "day")) ), fx @@ -161,15 +160,29 @@ class PlotH(APlot): def set_reach(self, reach_id): self._current_reach_id = reach_id self._current_profile_id = 0 - self.draw() + self.update() def set_profile(self, profile_id): self._current_profile_id = profile_id - self.draw() + self.update() def set_timestamp(self, timestamp): self._current_timestamp = timestamp - self.draw() + self.update() def update(self): - self.draw() + reach = self.results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id) + + x = self.ts + y = profile.get_key("Q") + + self._line.set_data(x, y) + + self._current.set_data( + self._current_timestamp, + y[self.ts.index(self._current_timestamp)] + ) + + self.canvas.axes.relim() + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py index 68f6fb4e..34ed91f9 100644 --- a/src/View/Results/PlotKPC.py +++ b/src/View/Results/PlotKPC.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,17 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotKPC(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): + +class PlotKPC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotKPC, self).__init__( canvas=canvas, + trad=trad, data=results, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self._current_timestamp = max(results.get("timestamps")) @@ -43,6 +46,11 @@ class PlotKPC(APlot): def results(self): return self.data + @results.setter + def results(self, results): + self.data = results + self._current_timestamp = max(results.get("timestamps")) + @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -55,18 +63,19 @@ class PlotKPC(APlot): self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Elevation (m)"), - color='green', fontsize=11 + color='black', fontsize=11 ) self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "KP (m)"), - color='green', fontsize=11 + color='black', fontsize=11 ) kp = reach.geometry.get_kp() z_min = reach.geometry.get_z_min() + z_max = reach.geometry.get_z_max() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) self.line_kp_zmin = self.canvas.axes.plot( @@ -92,9 +101,21 @@ class PlotKPC(APlot): self.canvas.axes.fill_between( kp, z_min, water_z, - color='blue', alpha=0.5, interpolate=True + color='skyblue', alpha=0.7, interpolate=True ) + self.profile, = self.canvas.axes.plot( + [ + kp[self._current_profile_id], + kp[self._current_profile_id] + ], + [ + z_max[self._current_profile_id], + z_min[self._current_profile_id] + ], + color='red', lw=1. + ) + self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() if self.toolbar is not None: @@ -103,15 +124,32 @@ class PlotKPC(APlot): def set_reach(self, reach_id): self._current_reach_id = reach_id self._current_profile_id = 0 - self.draw() + self.update() def set_profile(self, profile_id): self._current_profile_id = profile_id - self.draw() + self.update_profil() def set_timestamp(self, timestamp): self._current_timestamp = timestamp - self.draw() + self.update() def update(self): self.draw() + + def update_profil(self): + reach = self.results.river.reach(self._current_reach_id) + kp = reach.geometry.get_kp() + z_min = reach.geometry.get_z_min() + z_max = reach.geometry.get_z_max() + self.profile.set_data( + [ + kp[self._current_profile_id], + kp[self._current_profile_id] + ], + [ + z_max[self._current_profile_id], + z_min[self._current_profile_id] + ] + ) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py index 62f4e290..1db320f9 100644 --- a/src/View/Results/PlotSedProfile.py +++ b/src/View/Results/PlotSedProfile.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,14 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotSedProfile(APlot): - def __init__(self, canvas=None, study=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): + +class PlotSedProfile(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotSedProfile, self).__init__( canvas=canvas, - data=study, - toolbar=toolbar + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self._results = results @@ -31,6 +34,16 @@ class PlotSedProfile(APlot): self._current_reach_id = reach_id self._current_profile_id = profile_id + @property + def results(self): + return self.data + + @results.setter + def results(self, results): + self.data = results + self._results = results + self._current_timestamp = max(results.get("timestamps")) + def get_zsl(self, profile): x = profile.geometry.get_station() z = profile.geometry.z() @@ -38,13 +51,13 @@ class PlotSedProfile(APlot): profiles_sl = list( map( lambda sl: sl[0], - profile.get_ts_key(self._current_timestamp, "sl") + profile.get_ts_key(self._current_timestamp, "sl")[0] ) ) profiles_sl_0 = list( map( lambda sl: sl[0], - profile.get_ts_key(0.0, "sl") + profile.get_ts_key(0.0, "sl")[0] ) ) @@ -57,8 +70,8 @@ class PlotSedProfile(APlot): cur_0 = [] for p in range(profile.geometry.number_points): - cur.append(profile_sl) - cur_0.append(profile_sl_0) + cur.append(profile_sl) + cur_0.append(profile_sl_0) sl.append(cur) sl_0.append(cur_0) @@ -104,7 +117,6 @@ class PlotSedProfile(APlot): return list(reversed(z_sl)) - @timer def draw(self): self.canvas.axes.cla() @@ -120,18 +132,18 @@ class PlotSedProfile(APlot): self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "X (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Height (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) x = profile.geometry.get_station() z = profile.geometry.z() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) z_sl = self.get_zsl(profile) @@ -155,7 +167,7 @@ class PlotSedProfile(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py index 657b70e8..b7f229c8 100644 --- a/src/View/Results/PlotSedReach.py +++ b/src/View/Results/PlotSedReach.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,14 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotSedReach(APlot): - def __init__(self, canvas=None, study=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): + +class PlotSedReach(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotSedReach, self).__init__( canvas=canvas, - data=study, - toolbar=toolbar + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self._results = results @@ -31,6 +34,16 @@ class PlotSedReach(APlot): self._current_reach_id = reach_id self._current_profile_id = profile_id + @property + def results(self): + return self.data + + @results.setter + def results(self, results): + self.data = results + self._results = results + self._current_timestamp = max(results.get("timestamps")) + # DEPRECATED version of sediment layser display # def _get_zsl(self, reach): # kp = reach.geometry.get_kp() @@ -111,14 +124,14 @@ class PlotSedReach(APlot): profiles_sl_0 = list( map( # Get SL list for profile p at time 0 (initial data) - lambda p: p.get_ts_key(0.0, "sl"), + lambda p: p.get_ts_key(0.0, "sl")[0], reach.profiles ) ) profiles_sl = list( map( # Get SL list for profile p at current time - lambda p: p.get_ts_key(self._current_timestamp, "sl"), + lambda p: p.get_ts_key(self._current_timestamp, "sl")[0], reach.profiles ) ) @@ -150,7 +163,10 @@ class PlotSedReach(APlot): z_sl = reduce( lambda acc, v: acc + [ list( - map(lambda x, y: y - x, v, acc[-1]) + map( + lambda x, y: y - x, + v, acc[-1] + ) ) ], sl_0, @@ -200,11 +216,11 @@ class PlotSedReach(APlot): self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "Kp (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Height (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) kp = reach.geometry.get_kp() @@ -233,7 +249,7 @@ class PlotSedReach(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index 3e7a88f3..3d0e2281 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -21,7 +21,7 @@ import logging from functools import reduce from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -31,14 +31,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotXY(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None, display_current=True): + +class PlotXY(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + display_current=True, parent=None): super(PlotXY, self).__init__( canvas=canvas, + trad=trad, data=results, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self.display_current = display_current @@ -54,6 +57,11 @@ class PlotXY(APlot): def results(self): return self.data + @results.setter + def results(self, results): + self.data = results + self._current_timestamp = max(results.get("timestamps")) + @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -75,17 +83,17 @@ class PlotXY(APlot): # Axes self.canvas.axes.set_xlabel( _translate("Results", "X (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( _translate("Results", "Y (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.axis("equal") kp = reach.geometry.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) # Draw line for each profile @@ -96,9 +104,9 @@ class PlotXY(APlot): markersize=3, marker='+' ) for x, y, kp in zip( - reach.geometry.get_x(), - reach.geometry.get_y(), - kp + reach.geometry.get_x(), + reach.geometry.get_y(), + kp ) ] @@ -121,53 +129,26 @@ class PlotXY(APlot): profile.x(), profile.y(), lw=1., markersize=3, - marker='+', color="b" + color="r", marker='+' ) - self.plot_selected.set_visible(False) + self.plot_selected.set_visible(True) - # Display point under water - poly_l_x = [] - poly_l_y = [] - poly_r_x = [] - poly_r_y = [] - for profile in reach.profiles: - water_z = profile.get_ts_key( - self._current_timestamp, "Z" - ) - pgeo = profile.geometry + poly_x = [0] + poly_y = [0] - x, y = reduce( - lambda acc, pts: (acc[0] + [pts.x], acc[1] + [pts.y]), - filter( - lambda pts: pts.z < water_z, - pgeo.points - ), - ([], []) - ) + self.fill = self.canvas.axes.fill( + poly_x, poly_y, + color='skyblue', + alpha=0.7 + ) - poly_l_x.append(x[0]) - poly_l_y.append(y[0]) - poly_r_x.append(x[-1]) - poly_r_y.append(y[-1]) - - self.canvas.axes.plot( - x, y, lw=1., - color='b', - markersize=1, - marker='o' - ) - - poly_x = poly_l_x + list(reversed(poly_r_x)) + [poly_l_x[0]] - poly_y = poly_l_y + list(reversed(poly_r_y)) + [poly_l_y[0]] - - self.canvas.axes.fill(poly_x, poly_y, color='blue', alpha=1) - - self.canvas.axes.autoscale_view(True, True, True) - self.canvas.axes.autoscale() + # self.canvas.axes.autoscale_view(True, True, True) + # self.canvas.axes.autoscale() self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() if self.toolbar is not None: self.toolbar.update() + self.update() def set_reach(self, reach_id): self._current_reach_id = reach_id @@ -176,11 +157,64 @@ class PlotXY(APlot): def set_profile(self, profile_id): self._current_profile_id = profile_id - self.draw() + self.update_profile() def set_timestamp(self, timestamp): self._current_timestamp = timestamp - self.draw() + self.update_poly() + + def update_profile(self): + + reach = self.results.river.reach(self._current_reach_id) + if self.display_current: + # Current profile + profile = reach.profile(self._current_profile_id).geometry + + self.plot_selected.set_data(profile.x(), profile.y()) + self.plot_selected.set_visible(True) + self.canvas.draw_idle() + + def update_poly(self): + + reach = self.results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id).geometry + + # Display water + poly_l_x = [] + poly_l_y = [] + poly_r_x = [] + poly_r_y = [] + for profile in reach.profiles: + water_z = profile.get_ts_key( + self._current_timestamp, "Z" + ) + ptX = profile.get_ts_key( + self._current_timestamp, "ptX" + ) + ptY = profile.get_ts_key( + self._current_timestamp, "ptY" + ) + + poly_l_x.append(ptX.x) + poly_l_y.append(ptX.y) + poly_r_x.append(ptY.x) + poly_r_y.append(ptY.y) + + # self.canvas.axes.plot( + # x, y, lw=1., + # color='b', + # markersize=1, + # marker='o' + # ) + + poly_x = poly_l_x + list(reversed(poly_r_x)) + poly_y = poly_l_y + list(reversed(poly_r_y)) + poly = [] + for i in range(len(poly_x)): + poly.append([poly_x[i], poly_y[i]]) + self.fill[0].set_xy(poly) + self.canvas.draw_idle() def update(self): - self.draw() + self.update_profile() + self.update_poly() diff --git a/src/View/Results/Table.py b/src/View/Results/Table.py index 8e167459..f4d7b81a 100644 --- a/src/View/Results/Table.py +++ b/src/View/Results/Table.py @@ -34,6 +34,7 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel from View.Results.translate import * logger = logging.getLogger() @@ -41,33 +42,16 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, results = None, study = None, mode = None, undo=None): - super(QAbstractTableModel, self).__init__() +class TableModel(PamhyrTableModel): + def _setup_lst(self): + _river = self._data.river + if self._opt_data == "reach": + self._lst = _river.reachs + else: + self._lst = _river.reach(0).profiles - self._results = results - self._study = study - self._mode = mode - self._undo_stack = undo - - self._table_headers = table_headers_reach - if mode != "reach": - self._table_headers = table_headers_profile - - self._headers = list(self._table_headers.keys()) - - self._selected = 0 - self._timestamp = 0 - - def rowCount(self, parent=QModelIndex()): - if self._mode == "reach": - return len(self._results.river.reachs) - - current_reach = self._results.river.reach(self._selected) - return len(current_reach.profiles) - - def columnCount(self, parent=QModelIndex()): - return len(self._headers) + def __init__(self, **kwargs): + super(TableModel, self).__init__(**kwargs) def data(self, index, role=Qt.DisplayRole): if role != Qt.ItemDataRole.DisplayRole: @@ -76,32 +60,23 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._mode == "reach": + if self._opt_data == "reach": if self._headers[column] == "name": - v = self._results.river.reach(row).name + v = self._lst[row].name return str(v) else: - current_reach = self._results.river.reach(self._selected) if self._headers[column] == "name": - v = current_reach.profile(row).name + v = self._lst[row].name return str(v) elif self._headers[column] == "kp": - v = current_reach.profile(row).kp + v = self._lst[row].kp return f"{v:.4f}" return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return self._table_headers[self._headers[section]] - - return QVariant() - - def index(self, row, column, parent=QModelIndex()): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - return self.createIndex(row, column, QModelIndex()) - - def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsSelectable + def update(self, reach): + _river = self._data.river + if self._opt_data == "reach": + self._lst = _river.reachs + else: + self._lst = _river.reach(reach).profiles diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 251f5a13..bbb62e85 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -16,22 +16,22 @@ # -*- coding: utf-8 -*- +import os import logging -from datetime import datetime +from datetime import datetime from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( - QKeySequence, + QKeySequence, QIcon, QPixmap, ) from PyQt5.QtCore import ( Qt, QVariant, QAbstractTableModel, QCoreApplication, QModelIndex, pyqtSlot, - QItemSelectionModel, + QItemSelectionModel, QTimer, ) from PyQt5.QtWidgets import ( @@ -39,11 +39,11 @@ from PyQt5.QtWidgets import ( QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, QComboBox, QVBoxLayout, QHeaderView, QTabWidget, - QSlider, QLabel, + QSlider, QLabel, QWidget, QGridLayout, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.Results.PlotXY import PlotXY from View.Results.PlotAC import PlotAC @@ -52,75 +52,76 @@ from View.Results.PlotH import PlotH from View.Results.PlotSedReach import PlotSedReach from View.Results.PlotSedProfile import PlotSedProfile -from View.Results.Table import TableModel -from View.Results.translate import ( - table_headers_reach, table_headers_profile, - retranslate, +from View.Results.CustomPlot.Plot import CustomPlot +from View.Results.CustomPlot.CustomPlotValuesSelectionDialog import ( + CustomPlotValuesSelectionDialog, ) + +from View.Results.Table import TableModel +from View.Results.translate import ResultsTranslate from View.Stricklers.Window import StricklersWindow _translate = QCoreApplication.translate logger = logging.getLogger() -class ResultsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Results", - study=None, solver=None, results=None, + +class ResultsWindow(PamhyrWindow): + _pamhyr_ui = "Results" + _pamhyr_name = "Results" + + def _path_file(self, filename): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", filename + ) + ) + + def __init__(self, study=None, config=None, + solver=None, results=None, parent=None): - self._study = study self._solver = solver self._results = results self._timestamps = sorted(list(self._results.get("timestamps"))) - self.setup_title(title) - - super(ResultsWindow, self).__init__( - name=title, ui="Results", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._solver.name ) - self.setup_sc() + super(ResultsWindow, self).__init__( + title=name, + study=study, + config=config, + trad=ResultsTranslate(), + parent=parent + ) + + self._hash_data.append(self._solver) + self._hash_data.append(self._results) + + self._additional_plot = {} + self.setup_table() - self.setup_graph() + self.setup_plots() self.setup_slider() self.setup_statusbar() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._solver.name + " - " - + self._results.date - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() self._table = {} - for t in ["reach", "profile"]: table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - results = self._results, - study = self._study, - mode = t, - undo = self._undo_stack, + table_view=table, + table_headers=self._trad.get_dict(f"table_headers_{t}"), + data=self._results, + undo=self._undo_stack, + opt_data=t ) - table.setModel(self._table[t]) - - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) def setup_slider(self): self._slider_profile = self.find(QSlider, f"verticalSlider_profile") @@ -132,7 +133,25 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._slider_time.setMaximum(len(self._timestamps) - 1) self._slider_time.setValue(len(self._timestamps) - 1) - def setup_graph(self): + file_path = os.path.abspath(os.path.dirname(__file__)) + + self._icon_start = QIcon() + self._icon_start.addPixmap( + QPixmap(f'{file_path}/../ui/ressources/media-playback-start.png') + ) + self._icon_pause = QIcon() + self._icon_pause.addPixmap( + QPixmap(f'{file_path}/../ui/ressources/media-playback-pause.png') + ) + self._button_play = self.find(QPushButton, f"playButton") + self._button_play.setIcon(self._icon_start) + self._button_back = self.find(QPushButton, f"backButton") + self._button_next = self.find(QPushButton, f"nextButton") + self._button_first = self.find(QPushButton, f"firstButton") + self._button_last = self.find(QPushButton, f"lastButton") + self._timer = QTimer(self) + + def setup_plots(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( @@ -143,12 +162,12 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout.addWidget(self.canvas) self.plot_xy = PlotXY( - canvas = self.canvas, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar, - display_current = False + canvas=self.canvas, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar, + display_current=True ) self.plot_xy.draw() @@ -162,11 +181,11 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout_2.addWidget(self.canvas_2) self.plot_kpc = PlotKPC( - canvas = self.canvas_2, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_2 + canvas=self.canvas_2, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_2 ) self.plot_kpc.draw() @@ -180,11 +199,11 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout_3.addWidget(self.canvas_3) self.plot_ac = PlotAC( - canvas = self.canvas_3, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_3 + canvas=self.canvas_3, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_3 ) self.plot_ac.draw() @@ -193,16 +212,17 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.toolbar_4 = PamhyrPlotToolbar( self.canvas_4, self ) - self.plot_layout_4 = self.find(QVBoxLayout, "verticalLayout_hydrograph") + self.plot_layout_4 = self.find( + QVBoxLayout, "verticalLayout_hydrograph") self.plot_layout_4.addWidget(self.toolbar_4) self.plot_layout_4.addWidget(self.canvas_4) self.plot_h = PlotH( - canvas = self.canvas_4, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_4 + canvas=self.canvas_4, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_4 ) self.plot_h.draw() @@ -217,12 +237,11 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): if self._study.river.has_sediment(): self.plot_sed_reach = PlotSedReach( - canvas = self.canvas_5, - results = self._results, - study = self._study, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_5 + canvas=self.canvas_5, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_5 ) self.plot_sed_reach.draw() @@ -231,21 +250,25 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.toolbar_6 = PamhyrPlotToolbar( self.canvas_6, self ) - self.plot_layout_6 = self.find(QVBoxLayout, "verticalLayout_sed_profile") + self.plot_layout_6 = self.find( + QVBoxLayout, "verticalLayout_sed_profile") self.plot_layout_6.addWidget(self.toolbar_6) self.plot_layout_6.addWidget(self.canvas_6) if self._study.river.has_sediment(): self.plot_sed_profile = PlotSedProfile( - canvas = self.canvas_6, - results = self._results, - study = self._study, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_6 + canvas=self.canvas_6, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_6 ) self.plot_sed_profile.draw() + def closeEvent(self, event): + self._timer.stop() + super(ResultsWindow, self).closeEvent(event) + def _compute_status_label(self): # Timestamp ts = self._timestamps[self._slider_time.value()] @@ -289,11 +312,18 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._status_label.setText(txt) def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) + # Action + actions = { + "action_reload": self._reload, + "action_add": self._add_custom_plot + } + for action in actions: + self.find(QAction, action).triggered.connect( + actions[action] + ) + + # Table and Plot fun = { "reach": self._set_current_reach, "profile": self._set_current_profile, @@ -308,8 +338,16 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._table[t].dataChanged.connect(fun[t]) - self._slider_profile.valueChanged.connect(self._set_current_profile_slider) + self._slider_profile.valueChanged.connect( + self._set_current_profile_slider) self._slider_time.valueChanged.connect(self._set_current_timestamp) + self._button_play.setChecked(False) + self._button_play.clicked.connect(self._pause) + self._button_back.clicked.connect(self._back) + self._button_next.clicked.connect(self._next) + self._button_first.clicked.connect(self._first) + self._button_last.clicked.connect(self._last) + self._timer.timeout.connect(self._update_slider) def update_table_selection_reach(self, ind): table = self.find(QTableView, f"tableView_reach") @@ -337,7 +375,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): ) table.scrollTo(index) - def update(self, reach_id = None, profile_id = None, timestamp = None): + def update(self, reach_id=None, profile_id=None, timestamp=None): if reach_id is not None: self.plot_xy.set_reach(reach_id) self.plot_ac.set_reach(reach_id) @@ -348,6 +386,9 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_sed_reach.set_reach(reach_id) self.plot_sed_profile.set_reach(reach_id) + for plot in self._additional_plot: + self._additional_plot[plot].set_reach(reach_id) + self.update_table_selection_reach(reach_id) self.update_table_selection_profile(0) @@ -361,17 +402,83 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_sed_reach.set_profile(profile_id) self.plot_sed_profile.set_profile(profile_id) + for plot in self._additional_plot: + self._additional_plot[plot].set_profile(profile_id) + self.update_table_selection_profile(profile_id) + if timestamp is not None: self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) - # self.plot_h.set_timestamp(timestamp) + self.plot_h.set_timestamp(timestamp) if self._study.river.has_sediment(): self.plot_sed_reach.set_timestamp(timestamp) self.plot_sed_profile.set_timestamp(timestamp) + for plot in self._additional_plot: + self._additional_plot[plot].set_timestamp(timestamp) + + self.update_statusbar() + + def _get_current_reach(self): + table = self.find(QTableView, f"tableView_reach") + indexes = table.selectedIndexes() + if len(indexes) == 0: + return 0 + + return indexes[0].row() + + def _get_current_profile(self): + table = self.find(QTableView, f"tableView_profile") + indexes = table.selectedIndexes() + if len(indexes) == 0: + return 0 + + return indexes[0].row() + + def _get_current_timestamp(self): + return self._timestamps[ + self._slider_time.value() + ] + + def _set_current_reach(self): + table = self.find(QTableView, f"tableView_reach") + indexes = table.selectedIndexes() + if len(indexes) == 0: + return + + self.update(reach_id=indexes[0].row()) + + def _set_current_profile(self): + table = self.find(QTableView, f"tableView_profile") + indexes = table.selectedIndexes() + if len(indexes) == 0: + return + + ind = indexes[0].row() + self.update(profile_id=ind) + self._slider_profile.setValue(ind) + + def _set_current_profile_slider(self): + pid = self._slider_profile.value() + self.update(profile_id=pid) + + def _set_current_timestamp(self): + timestamp = self._timestamps[self._slider_time.value()] + self.update(timestamp=timestamp) + + def _reload_plots(self): + self.plot_xy.results = self._results + self.plot_ac.results = self._results + self.plot_kpc.results = self._results + self.plot_h.results = self._results + + if self._study.river.has_sediment(): + self.plot_sed_reach.results = self._results + self.plot_sed_profile.results = self._results + self.plot_xy.draw() self.plot_ac.draw() self.plot_kpc.draw() @@ -381,44 +488,113 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_sed_reach.draw() self.plot_sed_profile.draw() - self.update_statusbar() + def _reload_slider(self): + self._slider_time = self.find(QSlider, f"horizontalSlider_time") + self._slider_time.setMaximum(len(self._timestamps) - 1) + self._slider_time.setValue(len(self._timestamps) - 1) - def _set_current_reach(self): - table = self.find(QTableView, f"tableView_reach") - indexes = table.selectedIndexes() - if len(indexes) == 0: + def _reload(self): + logger.debug("Reload results...") + self._results = self._results.reload() + + self._timestamps = sorted(list(self._results.get("timestamps"))) + + self._reload_plots() + self._reload_slider() + + def _add_custom_plot(self): + dlg = CustomPlotValuesSelectionDialog(parent=self) + if dlg.exec(): + x, y = dlg.value + self.create_new_tab_custom_plot(x, y) + + def create_new_tab_custom_plot(self, x: str, y: list): + name = f"{x}: {','.join(y)}" + wname = f"tab_custom_{x}_{y}" + + tab_widget = self.find(QTabWidget, f"tabWidget") + + # This plot already exists + if name in self._additional_plot: + tab_widget.setCurrentWidget( + tab_widget.findChild(QWidget, wname) + ) return - self.update(reach_id = indexes[0].row()) + widget = QWidget() + grid = QGridLayout() - def _set_current_profile(self): - table = self.find(QTableView, f"tableView_profile") - indexes = table.selectedIndexes() - if len(indexes) == 0: - return + widget.setObjectName(wname) - ind = indexes[0].row() - self.update(profile_id = ind) - self._slider_profile.setValue(ind) + canvas = MplCanvas(width=5, height=4, dpi=100) + canvas.setObjectName(f"canvas_{x}_{y}") + toolbar = PamhyrPlotToolbar( + canvas, self + ) - def _set_current_profile_slider(self): - pid = self._slider_profile.value() - self.update(profile_id = pid) + plot = CustomPlot( + x, y, + self._get_current_reach(), + self._get_current_profile(), + self._get_current_timestamp(), + data=self._results, + canvas=canvas, + toolbar=toolbar, + parent=self, + ) + plot.draw() - return + # Add plot to additional plot + self._additional_plot[name] = plot - def _set_current_timestamp(self): - timestamp = self._timestamps[self._slider_time.value()] - self.update(timestamp = timestamp) + grid.addWidget(toolbar, 0, 0) + grid.addWidget(canvas, 1, 0) + widget.setLayout(grid) + tab_widget.addTab(widget, name) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() + + # play / pause buttons + def _update_slider(self): + if self._slider_time.value() == self._slider_time.maximum(): + self._slider_time.setValue(self._slider_time.minimum()) + else: + self._slider_time.setValue(self._slider_time.value()+1) + + def _next(self): + self._slider_time.setValue(self._slider_time.value()+1) + + def _back(self): + self._slider_time.setValue(self._slider_time.value()-1) + + def _first(self): + self._slider_time.setValue(self._slider_time.minimum()) + + def _last(self): + self._slider_time.setValue(self._slider_time.maximum()) + + def _pause(self): + if self._button_play.isChecked(): + self._button_next.setEnabled(False) + self._button_back.setEnabled(False) + self._button_first.setEnabled(False) + self._button_last.setEnabled(False) + self._timer.start(100) + self._button_play.setIcon(self._icon_pause) + else: + self._timer.stop() + self._button_next.setEnabled(True) + self._button_back.setEnabled(True) + self._button_first.setEnabled(True) + self._button_last.setEnabled(True) + self._button_play.setIcon(self._icon_start) diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py index 37351b66..4de61cc7 100644 --- a/src/View/Results/translate.py +++ b/src/View/Results/translate.py @@ -18,19 +18,27 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers_reach = { - "name": _translate("Results", "Reach name"), -} -table_headers_profile = { - "name": _translate("Results", "Name"), - "kp": _translate("Results", "KP (m)"), -} +class ResultsTranslate(PamhyrTranslate): + def __init__(self): + super(ResultsTranslate, self).__init__() -def retranslate(): - table_headers_reach["name"] = _translate("Results", "Reach name") + self._dict['day'] = _translate( + "Results", "day" + ) + self._dict['days'] = _translate( + "Results", "days" + ) - table_headers_profile["name"] = _translate("Results", "Name") - table_headers_profile["kp"] = _translate("Results", "KP (m)") + self._sub_dict["table_headers_reach"] = { + "name": _translate("Results", "Reach name"), + } + + self._sub_dict["table_headers_profile"] = { + "name": _translate("Results", "Name"), + "kp": _translate("Results", "KP (m)"), + } diff --git a/src/View/RunSolver/Log/Window.py b/src/View/RunSolver/Log/Window.py index 93d42165..3e343a45 100644 --- a/src/View/RunSolver/Log/Window.py +++ b/src/View/RunSolver/Log/Window.py @@ -22,8 +22,7 @@ import os from queue import Queue from tools import trace, timer -from View.ASubWindow import ASubWindow, ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -45,24 +44,24 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate -class SolverLogFileWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver logs", - file_name = None, + +class SolverLogFileWindow(PamhyrWindow): + _pamhyr_ui = "SolverLogFile" + _pamhyr_name = "Solver logs" + + def __init__(self, file_name=None, study=None, config=None, solver=None, parent=None): - self._title = title - self._parent = parent - - self._study = study - self._config = config self._solver = solver - self._file_name = file_name super(SolverLogFileWindow, self).__init__( - name=self._title, ui="SolverLogFile", parent=parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) - self.ui.setWindowTitle(self._title) self.setup_action() self.setup_connections() @@ -77,7 +76,8 @@ class SolverLogFileWindow(ASubMainWindow, ListedSubWindow): def setup_connections(self): self.find(QAction, "action_revert").triggered.connect(self.revert) - self.find(QAction, "action_open_in_editor").triggered.connect(self.open_on_editor) + self.find(QAction, "action_open_in_editor").triggered.connect( + self.open_on_editor) def setup_text(self): with open(self._file_name, "r") as f: diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 226d4da5..b950b929 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -21,10 +21,9 @@ import logging import tempfile from queue import Queue -from tools import trace, timer +from tools import trace, timer, logger_exception -from View.ASubWindow import ASubWindow, ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -49,40 +48,69 @@ from View.RunSolver.Log.Window import SolverLogFileWindow try: from signal import SIGTERM, SIGSTOP, SIGCONT _signal = True -except: +except Exception: _signal = False _translate = QCoreApplication.translate logger = logging.getLogger() -class SelectSolverWindow(ASubWindow, ListedSubWindow): - def __init__(self, title="Select solver", - study=None, config=None, - parent=None): - self._title = title - self._study = study - self._config = config +class SelectSolverWindow(PamhyrDialog): + _pamhyr_ui = "SelectSolver" + _pamhyr_name = "Select solver" + + def __init__(self, study=None, config=None, + parent=None): self._solver = None super(SelectSolverWindow, self).__init__( - name=self._title, ui="SelectSolver", parent=parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) - self.ui.setWindowTitle(self._title) self.setup_combobox() self.setup_connections() + self.select_last_solver() def setup_combobox(self): solvers = self._config.solvers - solvers_name = list(map(lambda s: s.name + f" - ({s._type})", solvers)) + solvers_name = list( + map( + self._format_solver_name, + solvers + ) + ) self.combobox_add_items("comboBox", solvers_name) def setup_connections(self): self.find(QPushButton, "pushButton_run").clicked.connect(self.accept) - self.find(QPushButton, "pushButton_cancel").clicked.connect(self.reject) + self.find(QPushButton, "pushButton_cancel")\ + .clicked.connect(self.reject) + + def select_last_solver(self): + solvers = self._config.solvers + last = self._config.last_solver_name + + solver = list( + filter( + lambda s: s.name == last, + solvers + ) + ) + + if len(solver) != 0: + self.set_combobox_text( + "comboBox", + self._format_solver_name(solver[0]) + ) + + def _format_solver_name(self, solver): + return f"{solver.name} - ({solver._type})" @property def solver(self): @@ -92,6 +120,8 @@ class SelectSolverWindow(ASubWindow, ListedSubWindow): solver_name = self.get_combobox_text("comboBox") solver_name = solver_name.rsplit(" - ", 1)[0] + self._config.update_last_solver_used(solver_name) + self._solver = next( filter( lambda s: s.name == solver_name, @@ -102,60 +132,31 @@ class SelectSolverWindow(ASubWindow, ListedSubWindow): super(SelectSolverWindow, self).accept() -class SolverLogWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver logs", - study=None, config=None, +class SolverLogWindow(PamhyrWindow): + _pamhyr_ui = "SolverLog" + _pamhyr_name = "Solver Log" + + def __init__(self, study=None, config=None, solver=None, parent=None): - self._title = title - self._parent = parent - - self._study = study - self._config = config self._solver = solver - self._results = None super(SolverLogWindow, self).__init__( - name=self._title, ui="SolverLog", parent=parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) - self.ui.setWindowTitle(self._title) self.setup_action() self.setup_alarm() self.setup_connections() + self.setup_workdir() + self.setup_process() - self._workdir = "" - if self._study.filename == "": - self._workdir = tempfile.TemporaryDirectory() - else: - self._workdir = os.path.join( - os.path.dirname(self._study.filename), - "_PAMHYR_", - self._study.name.replace(" ", "_"), - self._solver.name.replace(" ", "_"), - ) - os.makedirs(self._workdir, exist_ok = True) - - self._alarm.start(500) - self._output = Queue() - self._process = self.new_process(parent) - - self._log(f" *** Export study {self._solver.name}", color="blue") - self._solver.export(self._study, self._workdir, qlog = self._output) - - self.update() - - self._log(f" *** Run solver {self._solver.name}", color="blue") - self._solver.run( - study, - process = self._process, - output_queue = self._output - ) - - def new_process(self, parent): - new = QProcess(parent) - new.setWorkingDirectory(self._workdir) - return new + self.export() + self.run() def setup_action(self): self.find(QAction, "action_start").setEnabled(False) @@ -180,21 +181,77 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self._alarm.timeout.connect(self.update) + def setup_workdir(self): + self._workdir = "" + if self._study.filename == "": + self._workdir = tempfile.TemporaryDirectory() + else: + self._workdir = os.path.join( + os.path.dirname(self._study.filename), + "_PAMHYR_", + self._study.name.replace(" ", "_"), + self._solver.name.replace(" ", "_"), + ) + os.makedirs(self._workdir, exist_ok=True) + + def setup_process(self): + self._alarm.start(100) + self._output = Queue() + self._process = self.new_process(self._parent) + + def new_process(self, parent): + new = QProcess(parent) + new.setWorkingDirectory(self._workdir) + new.setProcessChannelMode(QProcess.MergedChannels) + return new + + def export(self): + self._log(f" *** Export study {self._solver.name}", color="blue") + self._solver.export(self._study, self._workdir, qlog=self._output) + self.update() + + def closeEvent(self, event): + self._alarm.stop() + super(SolverLogWindow, self).closeEvent(event) + + ####### + # LOG # + ####### + def _log(self, msg, color=None): - if type(msg) == str: - msg = msg.rsplit('\n')[0] + if type(msg) is str: + self._log_str(msg, color) + elif type(msg) is int: + self._log_int(msg, color) - if color is not None: - msg = f"" + msg + "" + def _log_str(self, msg, color=None): + logger.info(f"solver: {msg}") + msg = msg.rsplit('\n')[0] - self.find(QTextEdit, "textEdit").append(msg) + if color is not None: + msg = f"" + msg + "" - elif type(msg) == int: - color = "blue" if msg == 0 else "red" - self.find(QTextEdit, "textEdit")\ - .append(f"" + - f" *** Finished with code {msg}" + - "") + self.find(QTextEdit, "textEdit").append(msg) + + def _log_int(self, int_code, color=None): + logger.info(f"solver: Returns {int_code}") + color = "blue" if int_code == 0 else "red" + + self.find(QTextEdit, "textEdit")\ + .append( + f"" + + f" *** Finished with code {int_code}" + + "" + ) + + self.statusbar.showMessage( + "Done" if int_code == 0 else "Failed", + 3000 + ) + + ########## + # UPDATE # + ########## def update(self): if self._solver.is_stoped(): @@ -206,24 +263,54 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) + self._update_logs_all() + self._update_get_results() + + self._update_logs_all() + + def _update_get_results(self): + if self._results is None: + try: + self._results = self._solver.results( + self._study, self._workdir, qlog=self._output + ) + self._parent.set_results(self._solver, self._results) + except Exception as e: + logger.error(f"Failed to open results") + logger_exception(e) + + def _update_logs_all(self): while self._output.qsize() != 0: s = self._output.get() + if type(s) is str and "[ERROR]" in s: self._log(s, color="red") else: self._log(s) + #################### + # Process controle # + #################### + + def run(self): + self._log(f" *** Run solver {self._solver.name}", color="blue") + self._solver.run( + self._study, + process=self._process, + output_queue=self._output + ) + def start(self): if self._solver.is_stoped(): self._log(f" *** Export study {self._solver.name}", color="blue") - self._solver.export(self._study, self._workdir, qlog = self._output) + self._solver.export(self._study, self._workdir, qlog=self._output) self.update() self._process = self.new_process(self._parent) self._log(" *** Start", color="blue") self._results = None - self._solver.start(self._study, process = self._process) + self._solver.start(self._study, process=self._process) self.find(QAction, "action_start").setEnabled(False) if _signal: @@ -254,20 +341,27 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) + ########### + # Results # + ########### + def results(self): if self._results is None: - self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + self._results = self._solver.results( + self._study, self._workdir, qlog=self._output) - self._parent.set_results(self._results) + self._parent.set_results(self._solver, self._results) self._parent.open_solver_results(self._solver, self._results) + self._solver.has_results_loaded() + def log_file(self): file_name = os.path.join(self._workdir, self._solver.log_file()) log = SolverLogFileWindow( - file_name = file_name, - study = self._study, - config = self._config, - solver = self._solver, - parent = self, + file_name=file_name, + study=self._study, + config=self._config, + solver=self._solver, + parent=self, ) log.show() diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index e48ec5eb..820dc46b 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -5,23 +5,21 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot - -from PyQt5.QtCore import ( - QCoreApplication -) - -_translate = QCoreApplication.translate +from View.Tools.PamhyrPlot import PamhyrPlot logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): + +class Plot(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, + toolbar=None, display_current=True, + parent=None): super(Plot, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self._display_current = display_current @@ -34,26 +32,22 @@ class Plot(APlot): self.canvas.axes.cla() self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) - # self.canvas.axes.set_xlabel( - # _translate("MainWindow_reach", "X (m)"), - # color='green', fontsize=12 - # ) self.canvas.axes.axes.get_xaxis().set_visible(False) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Height (m)"), - color='green', fontsize=12 + self._trad["height"], + color='black', fontsize=10 ) if self.data is None: return - x = [0,1] - z = [0,0] + x = [0, 1] + z = [0, 0] sl = self.data.height() names = ["bottom"] + self.data.names() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) lsl = [] @@ -83,7 +77,8 @@ class Plot(APlot): lw=1.8, color='grey' if i == len(names) - 1 else None ) - self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[-(i+1)]}') + self.canvas.axes.text( + x[0] + 0.01, zsl[0] + 0.01, f'{names[-(i+1)]}') self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() @@ -94,7 +89,7 @@ class Plot(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index e1a41cb2..ea7cd934 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -15,32 +15,14 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SedimentLayers.Edit.UndoCommand import * -from View.SedimentLayers.Edit.translate import * _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, study=None, sl=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._sl = sl - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._sl) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() @@ -49,23 +31,17 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._sl.get(row).name + return self._data.get(row).name elif self._headers[column] == "type": - return self._sl.get(row).type + return self._data.get(row).type elif self._headers[column] == "height": - return self._sl.get(row).height + return self._data.get(row).height elif self._headers[column] == "d50": - return self._sl.get(row).d50 + return self._data.get(row).d50 elif self._headers[column] == "sigma": - return self._sl.get(row).sigma + return self._data.get(row).sigma elif self._headers[column] == "critical_constraint": - return self._sl.get(row).critical_constraint - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.get(row).critical_constraint return QVariant() @@ -79,37 +55,37 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "type": self._undo.push( SetTypeCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "height": self._undo.push( SetHeightCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "d50": self._undo.push( SetD50Command( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "sigma": self._undo.push( SetSigmaCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "critical_constraint": self._undo.push( SetCriticalConstraintCommand( - self._sl, row, value + self._data, row, value ) ) @@ -121,7 +97,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row + self._data, row ) ) @@ -133,7 +109,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._sl, rows + self._data, rows ) ) @@ -150,7 +126,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._sl, "up", row + self._data, "up", row ) ) @@ -158,7 +134,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, row, parent=QModelIndex()): - if row + 1 >= len(self._sl): + if row + 1 >= len(self._data): return target = row @@ -167,7 +143,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._sl, "down", row + self._data, "down", row ) ) diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index 298fa529..e353211c 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -10,6 +10,7 @@ from PyQt5.QtWidgets import ( from Model.SedimentLayer.SedimentLayer import SedimentLayer from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + class SetNameCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -25,6 +26,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).name = self._new + class SetTypeCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -40,6 +42,7 @@ class SetTypeCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).type = self._new + class SetHeightCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -55,6 +58,7 @@ class SetHeightCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).height = self._new + class SetD50Command(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -70,6 +74,7 @@ class SetD50Command(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).d50 = self._new + class SetSigmaCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -85,6 +90,7 @@ class SetSigmaCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).sigma = self._new + class SetCriticalConstraintCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -100,6 +106,7 @@ class SetCriticalConstraintCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).critical_constraint = self._new + class AddCommand(QUndoCommand): def __init__(self, sediment_layers, index): QUndoCommand.__init__(self) @@ -117,6 +124,7 @@ class AddCommand(QUndoCommand): else: self._sediment_layers.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, sediment_layers, rows): QUndoCommand.__init__(self) @@ -136,6 +144,7 @@ class DelCommand(QUndoCommand): def redo(self): self._sediment_layers.delete_i(self._rows) + class MoveCommand(QUndoCommand): def __init__(self, sediment_layers, up, i): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index 29b9ddff..080eee3f 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -40,66 +39,59 @@ from PyQt5.QtWidgets import ( QComboBox, QVBoxLayout, QHeaderView, QTabWidget, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.SedimentLayers.Edit.UndoCommand import * from View.SedimentLayers.Edit.Table import * from View.SedimentLayers.Edit.Plot import Plot -from View.SedimentLayers.Edit.translate import table_headers, retranslate +from View.SedimentLayers.Edit.translate import SedimentEditTranslate _translate = QCoreApplication.translate logger = logging.getLogger() -class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit Sediment Layers", - study=None, sl=None, parent=None): - self._study = study + +class EditSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "EditSedimentLayers" + _pamhyr_name = "Edit Sediment Layers" + + def __init__(self, study=None, config=None, + sl=None, parent=None): self._sl = sl - self.setup_title(title) - - super(EditSedimentLayersWindow, self).__init__( - name=self._title, ui="EditSedimentLayers", parent=parent - ) - - self.setup_sc() - self.setup_table() - self.setup_graph() - self.setup_connections() - - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): name = self._sl.name if name == "": name = _translate("SedimentLayers", "(no name)") - self._title = ( - title + " - " + self._study.name + " - " + name + name = ( + self._pamhyr_name + " - " + study.name + " - " + name ) - def setup_sc(self): - self._undo_stack = QUndoStack() + super(EditSedimentLayersWindow, self).__init__( + title=name, + study=study, + config=config, + trad=SedimentEditTranslate(), + parent=parent + ) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self.setup_table() + self.setup_graph() + self.setup_connections() def setup_table(self): - retranslate() - self._table = {} - + table_headers = self._trad.get_dict("table_headers") table = self.find(QTableView, f"tableView") self._table = TableModel( - study = self._study, - sl = self._sl, - undo = self._undo_stack, + table_view=table, + table_headers=table_headers, + editable_headers=list(table_headers), + data=self._sl, + trad=self._trad, + undo=self._undo_stack, ) table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) @@ -117,24 +109,20 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def _set_plot(self): self.plot = Plot( - canvas = self.canvas, - data = self._sl, - toolbar = self.toolbar, - display_current = False + canvas=self.canvas, + data=self._sl, + toolbar=self.toolbar, + trad=self._trad, + display_current=False ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_move_up").triggered.connect(self.move_up) - self.find(QAction, "action_move_down").triggered.connect(self.move_down) - - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) + self.find(QAction, "action_move_down").triggered.connect( + self.move_down) self._table.dataChanged.connect(self._set_plot) self._table.layoutChanged.connect(self._set_plot) @@ -179,14 +167,14 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(rows[0]) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index a6b77897..8c174ea3 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -2,21 +2,23 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.translate import SedimentTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - # "type": _translate("SedimentLayers", "Type"), - "height": _translate("SedimentLayers", "Height"), - "d50": _translate("SedimentLayers", "D50"), - "sigma": _translate("SedimentLayers", "Sigma"), - "critical_constraint": _translate("SedimentLayers", "Critical constraint"), -} -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - # table_headers["type"] = _translate("SedimentLayers", "Type") - table_headers["height"] = _translate("SedimentLayers", "Height") - table_headers["d50"] = _translate("SedimentLayers", "D50") - table_headers["sigma"] = _translate("SedimentLayers", "Sigma") - table_headers["critical_constraint"] = _translate("SedimentLayers", "Critical constraint") +class SedimentEditTranslate(SedimentTranslate): + def __init__(self): + super(SedimentEditTranslate, self).__init__() + + self._dict["height"] = _translate("SedimentLayers", "Height (m)"), + + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + # "type": _translate("SedimentLayers", "Type"), + "height": _translate("SedimentLayers", "Height"), + "d50": _translate("SedimentLayers", "D50"), + "sigma": _translate("SedimentLayers", "Sigma"), + "critical_constraint": _translate("SedimentLayers", + "Critical constraint"), + } diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index a312a8ce..118af1db 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,13 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): + +class Plot(PamhyrPlot): + def __init__(self, data=None, display_current=True, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self._display_current = display_current @@ -41,12 +45,12 @@ class Plot(APlot): return self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "Kp (m)"), - color='green', fontsize=12 + self._trad["kp"], + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Height (m)"), - color='green', fontsize=12 + self._trad["height"], + color='black', fontsize=10 ) kp = self.data.get_kp() @@ -55,7 +59,7 @@ class Plot(APlot): z_max = self.data.get_z_max() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) # Compute sediment layer in function to profile z_min @@ -87,7 +91,7 @@ class Plot(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py index 50b4bba2..17a1a584 100644 --- a/src/View/SedimentLayers/Reach/Profile/Plot.py +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,13 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): + +class Plot(PamhyrPlot): + def __init__(self, data=None, display_current=True, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( canvas=canvas, + trad=trad, data=data, - toolbar=toolbar + toolbar=toolbar, + parent=parent ) self._display_current = display_current @@ -42,11 +46,11 @@ class Plot(APlot): self.canvas.axes.set_xlabel( _translate("MainWindow_reach", "X (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Height (m)"), - color='green', fontsize=12 + color='black', fontsize=10 ) x = self.data.get_station() @@ -54,7 +58,7 @@ class Plot(APlot): sl = self.data.get_sl() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) # Compute sediment layer in function to point z @@ -86,7 +90,7 @@ class Plot(APlot): @timer def update(self, ind=None): - if self._init == False: + if not self._init: self.draw() return diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index 102bec32..3754aa82 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -20,10 +20,13 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.Profile.UndoCommand import * from View.SedimentLayers.Reach.Profile.translate import * +from View.Tools.PamhyrTable import PamhyrTableModel + _translate = QCoreApplication.translate logger = logging.getLogger() + class ComboBoxDelegate(QItemDelegate): def __init__(self, study=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) @@ -58,8 +61,9 @@ class ComboBoxDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() @@ -67,28 +71,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, study=None, profile=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._profile = profile - - def flags(self, index): - column = index.column() - - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if self._headers[column] == "sl": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return self._profile.number_points - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -98,25 +84,19 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._profile.point(row).name + return self._data.point(row).name elif self._headers[column] == "sl": - value = self._profile.point(row).sl - if value == None: + value = self._data.point(row).sl + if value is None: text = _translate("SedimentLayers", "Not defined") return text return str(value) elif self._headers[column] == "x": - return self._profile.point(row).x + return self._data.point(row).x elif self._headers[column] == "y": - return self._profile.point(row).y + return self._data.point(row).y elif self._headers[column] == "z": - return self._profile.point(row).z - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.point(row).z return QVariant() @@ -139,7 +119,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SetSLCommand( - self._profile, row, new + self._data, row, new ) ) diff --git a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py index fe3497ae..980d970b 100644 --- a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py @@ -16,6 +16,7 @@ from Model.SedimentLayer.SedimentLayerList import SedimentLayerList logger = logging.getLogger() + class SetSLCommand(QUndoCommand): def __init__(self, profile, index, new_value): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index eaae59d3..22ca8fcb 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -4,8 +4,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -28,37 +27,45 @@ from View.SedimentLayers.Reach.Profile.UndoCommand import * from View.SedimentLayers.Reach.Profile.Table import * from View.SedimentLayers.Reach.Profile.Plot import Plot -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.SedimentLayers.Reach.Profile.translate import table_headers, retranslate +from View.SedimentLayers.Reach.Profile.translate import ( + SedimentProfileTranslate +) from View.SedimentLayers.Window import SedimentLayersWindow _translate = QCoreApplication.translate logger = logging.getLogger() -class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Profile sediment layers", study=None, profile=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers - self._profile = profile - self._reach = self._study.river.current_reach().reach - self.setup_title(title) +class ProfileSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "ProfileSedimentLayers" + _pamhyr_name = "Profile sediment layers" + + def __init__(self, study=None, config=None, profile=None, parent=None): + self._sediment_layers = study.river.sediment_layers + self._profile = profile + self._reach = profile.reach + + name = self.compute_name(study) super(ProfileSedimentLayersWindow, self).__init__( - name=self._title, ui="ProfileSedimentLayers", parent=parent + title=name, + study=study, + config=config, + trad=SedimentProfileTranslate(), + parent=parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_title(self, title): + def compute_name(self, study): rname = self._reach.name if rname == "": rname = _translate("SedimentLayers", "(no name)") @@ -68,43 +75,34 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): pname = _translate( "SedimentLayers", "(no name - @kp)").replace("@kp", str(self._profile.kp) - ) + ) - self._title = ( - title + " - " - + self._study.name + " - " + return ( + self._pamhyr_name + " - " + + study.name + " - " + rname + " - " + pname ) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() + table_headers = self._trad.get_dict("table_headers") - table = self.find(QTableView, f"tableView") - self._table = TableModel( - study = self._study, - profile = self._profile, - undo = self._undo_stack, - ) - table.setModel(self._table) - - self._delegate_stricklers = ComboBoxDelegate( - study = self._study, + self._delegate_sl = ComboBoxDelegate( + study=self._study, parent=self ) - table.setItemDelegateForColumn( - list(table_headers).index("sl"), - self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + data=self._profile, + opt_data=self._study, + table_headers=table_headers, + editable_headers=["sl"], + delegates={"sl": self._delegate_sl}, + trad=self._trad, + undo=self._undo_stack, ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) @@ -123,25 +121,16 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def _update_plot(self): self.plot = Plot( - canvas = self.canvas, - data = self._profile, - toolbar = self.toolbar, - display_current = False + canvas=self.canvas, + data=self._profile, + toolbar=self.toolbar, + display_current=False ) self.plot.draw() - def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - - self._table.layoutChanged\ - .connect(self._update_plot) - self._table.dataChanged\ - .connect(self._update_plot) - + self._table.layoutChanged.connect(self._update_plot) + self._table.dataChanged.connect(self._update_plot) def index_selected_rows(self): table = self.find(QTableView, f"tableView") @@ -155,14 +144,14 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) ) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py index da256cbe..81e6623d 100644 --- a/src/View/SedimentLayers/Reach/Profile/translate.py +++ b/src/View/SedimentLayers/Reach/Profile/translate.py @@ -2,19 +2,19 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.Reach.translate import SedimentReachTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("SedimentLayers", "X (m)"), - "y": _translate("SedimentLayers", "Y (m)"), - "z": _translate("SedimentLayers", "Z (m)"), - "name": _translate("SedimentLayers", "Name"), - "sl": _translate("SedimentLayers", "Sediment layers"), -} -def retranslate(): - table_headers["x"] = _translate("SedimentLayers", "X (m)") - table_headers["y"] = _translate("SedimentLayers", "Y (m)") - table_headers["z"] = _translate("SedimentLayers", "Z (m)") - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["sl"] = _translate("SedimentLayers", "Sediment layers") +class SedimentProfileTranslate(SedimentReachTranslate): + def __init__(self): + super(SedimentProfileTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "x": _translate("SedimentLayers", "X (m)"), + "y": _translate("SedimentLayers", "Y (m)"), + "z": _translate("SedimentLayers", "Z (m)"), + "name": _translate("SedimentLayers", "Name"), + "sl": _translate("SedimentLayers", "Sediment layers"), + } diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py index ce1e9794..9f3c878b 100644 --- a/src/View/SedimentLayers/Reach/SLDialog.py +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -20,12 +19,17 @@ from View.SedimentLayers.Reach.translate import * _translate = QCoreApplication.translate -class SLDialog(ASubWindow, ListedSubWindow): - def __init__(self, title="SL", study=None, parent=None): - self._study = study +class SLDialog(PamhyrDialog): + _pamhyr_ui = "SLDialog" + _pamhyr_name = "SL" + + def __init__(self, study=None, config=None, parent=None): super(SLDialog, self).__init__( - name=title, ui="SLDialog", parent=parent + title=self._pamhyr_name, + study=study, + config=config, + parent=parent ) self.setup_combobox() diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index 8fdb8d7e..71889408 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -18,23 +18,26 @@ from PyQt5.QtWidgets import ( ) from View.SedimentLayers.Reach.UndoCommand import * -from View.SedimentLayers.Reach.translate import * + +from View.Tools.PamhyrTable import PamhyrTableModel _translate = QCoreApplication.translate logger = logging.getLogger() + class ComboBoxDelegate(QItemDelegate): - def __init__(self, study=None, parent=None): + def __init__(self, study=None, trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._study = study + self._trad = trad def createEditor(self, parent, option, index): self.editor = QComboBox(parent) self.editor.addItems( - [_translate("SedimentLayers", "Not defined")] + + [self._trad["nd"]] + list( map( lambda sl: str(sl), @@ -58,8 +61,9 @@ class ComboBoxDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() @@ -67,28 +71,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, study=None, reach=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._reach = reach - - def flags(self, index): - column = index.column() - - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if self._headers[column] == "sl": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return self._reach.number_profiles - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -98,24 +84,18 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._reach.profile(row).name + return self._data.profile(row).name if self._headers[column] == "kp": - return self._reach.profile(row).kp + return self._data.profile(row).kp if self._headers[column] == "sl": - value = self._reach.profile(row).sl - if value == None: + value = self._data.profile(row).sl + if value is None: text = _translate("SedimentLayers", "Not defined") return text return str(value) return QVariant() - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -135,7 +115,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SetSLCommand( - self._reach, row, new + self._data, row, new ) ) @@ -145,7 +125,7 @@ class TableModel(QAbstractTableModel): def apply_sl_each_profile(self, sl): self._undo.push( ApplySLCommand( - self._reach, sl + self._data, sl ) ) self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py index 17e1f0f1..4bce2235 100644 --- a/src/View/SedimentLayers/Reach/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/UndoCommand.py @@ -16,6 +16,7 @@ from Model.SedimentLayer.SedimentLayerList import SedimentLayerList logger = logging.getLogger() + class SetSLCommand(QUndoCommand): def __init__(self, reach, index, new_value): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index d79312b2..854e06a5 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -4,8 +4,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -29,75 +28,78 @@ from View.SedimentLayers.Reach.Table import * from View.SedimentLayers.Reach.Plot import Plot from View.SedimentLayers.Reach.SLDialog import SLDialog -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.SedimentLayers.Reach.translate import table_headers, retranslate +from View.SedimentLayers.Reach.translate import SedimentReachTranslate from View.SedimentLayers.Window import SedimentLayersWindow -from View.SedimentLayers.Reach.Profile.Window import ProfileSedimentLayersWindow +from View.SedimentLayers.Reach.Profile.Window import ( + ProfileSedimentLayersWindow +) _translate = QCoreApplication.translate logger = logging.getLogger() -class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Reach sediment layers", study=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers - self._reach = self._study.river.current_reach().reach - self.setup_title(title) +class ReachSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "ReachSedimentLayers" + _pamhyr_name = "Reach sediment layers" + + def __init__(self, study=None, config=None, reach=None, parent=None): + self._sediment_layers = study.river.sediment_layers + if reach is None: + self._reach = study.river.current_reach().reach + else: + self._reach = reach + + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._reach.name + ) super(ReachSedimentLayersWindow, self).__init__( - name=self._title, ui="ReachSedimentLayers", parent=parent + title=name, + study=study, + config=config, + trad=SedimentReachTranslate(), + parent=parent ) - self.setup_sc() + # Add reach to hash computation data + self._hash_data.append(self._reach) + self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " + self._study.name + " - " + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() + table_headers = self._trad.get_dict("table_headers") - table = self.find(QTableView, f"tableView") - self._table = TableModel( - study = self._study, - reach = self._reach, - undo = self._undo_stack, - ) - table.setModel(self._table) - - self._delegate_stricklers = ComboBoxDelegate( - study = self._study, + self._delegate_sl = ComboBoxDelegate( + study=self._study, + trad=self._trad, parent=self ) - table.setItemDelegateForColumn( - list(table_headers).index("sl"), - self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + data=self._reach, + opt_data=self._study, + table_headers=table_headers, + editable_headers=["sl"], + delegates={"sl": self._delegate_sl}, + trad=self._trad, + undo=self._undo_stack, ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): + def setup_plot(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( @@ -111,14 +113,14 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def _update_plot(self): self.plot = Plot( - canvas = self.canvas, - data = self._reach, - toolbar = self.toolbar, - display_current = False + canvas=self.canvas, + data=self._reach, + toolbar=self.toolbar, + trad=self._trad, + display_current=False ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_edit").triggered.connect(self.edit_profile) @@ -129,17 +131,11 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): .clicked\ .connect(self.apply_sl_each_profile) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.layoutChanged\ .connect(self._update_plot) self._table.dataChanged\ .connect(self._update_plot) - def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -152,24 +148,24 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) ) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() self._update_plot() - def redo(self): + def _redo(self): self._table.redo() self._update_plot() def apply_sl_each_profile(self): slw = SLDialog( - study = self._study, - parent = self + study=self._study, + parent=self ) if slw.exec(): sl = slw.sl @@ -182,16 +178,15 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): for row in rows: slw = ProfileSedimentLayersWindow( - study = self._study, - profile = self._reach.profile(row), - parent = self + study=self._study, + profile=self._reach.profile(row), + parent=self ) slw.show() - def edit_sl(self): slw = SedimentLayersWindow( - study = self._study, - parent = self + study=self._study, + parent=self ) slw.show() diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py index ef45f076..fd2d3bdb 100644 --- a/src/View/SedimentLayers/Reach/translate.py +++ b/src/View/SedimentLayers/Reach/translate.py @@ -2,15 +2,21 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.translate import SedimentTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - "kp": _translate("SedimentLayers", "KP (m)"), - "sl": _translate("SedimentLayers", "Sediment layers"), -} -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["kp"] = _translate("SedimentLayers", "KP (m)") - table_headers["sl"] = _translate("SedimentLayers", "Sediment layers") +class SedimentReachTranslate(SedimentTranslate): + def __init__(self): + super(SedimentReachTranslate, self).__init__() + + self._dict["nd"] = _translate("SedimentLayers", "Not defined") + self._dict["kp"] = _translate("SedimentLayers", "Kp (m)") + self._dict["height"] = _translate("SedimentLayers", "Height (m)") + + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + "kp": _translate("SedimentLayers", "KP (m)"), + "sl": _translate("SedimentLayers", "Sediment layers"), + } diff --git a/src/View/SedimentLayers/Table.py b/src/View/SedimentLayers/Table.py index f375c318..2b7a00f6 100644 --- a/src/View/SedimentLayers/Table.py +++ b/src/View/SedimentLayers/Table.py @@ -15,32 +15,12 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SedimentLayers.UndoCommand import * -from View.SedimentLayers.translate import * - -_translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, study=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._sl = self._study.river.sediment_layers - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._sl) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() @@ -49,15 +29,9 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._sl.get(row).name + return self._data.get(row).name elif self._headers[column] == "comment": - return self._sl.get(row).comment - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.get(row).comment return QVariant() @@ -71,13 +45,13 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "comment": self._undo.push( SetCommentCommand( - self._sl, row, value + self._data, row, value ) ) @@ -89,7 +63,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row + self._data, row ) ) @@ -101,7 +75,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._sl, rows + self._data, rows ) ) @@ -118,7 +92,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sl, "up", row + self._data, "up", row ) ) @@ -126,7 +100,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._sl): + if row > len(self._data): return target = row @@ -135,7 +109,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sl, "down", row + self._data, "down", row ) ) diff --git a/src/View/SedimentLayers/UndoCommand.py b/src/View/SedimentLayers/UndoCommand.py index f785743b..379a9545 100644 --- a/src/View/SedimentLayers/UndoCommand.py +++ b/src/View/SedimentLayers/UndoCommand.py @@ -10,6 +10,7 @@ from PyQt5.QtWidgets import ( from Model.SedimentLayer.SedimentLayer import SedimentLayer from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + class SetNameCommand(QUndoCommand): def __init__(self, sediment_layers_list, index, new_value): QUndoCommand.__init__(self) @@ -25,6 +26,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._sediment_layers_list.get(self._index).name = self._new + class SetCommentCommand(QUndoCommand): def __init__(self, sediment_layers_list, index, new_value): QUndoCommand.__init__(self) @@ -40,6 +42,7 @@ class SetCommentCommand(QUndoCommand): def redo(self): self._sediment_layers_list.get(self._index).comment = self._new + class AddCommand(QUndoCommand): def __init__(self, sediment_layers_list, index): QUndoCommand.__init__(self) @@ -57,6 +60,7 @@ class AddCommand(QUndoCommand): else: self._sediment_layers_list.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, sediment_layers_list, rows): QUndoCommand.__init__(self) @@ -76,6 +80,7 @@ class DelCommand(QUndoCommand): def redo(self): self._sediment_layers_list.delete_i(self._rows) + class MoveCommand(QUndoCommand): def __init__(self, sediment_layers_list, up, i): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index 6c14ae6c..1ff2c7f9 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -1,11 +1,26 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -29,62 +44,54 @@ from View.SedimentLayers.Table import * from View.SedimentLayers.Edit.Plot import Plot -from View.Plot.MplCanvas import MplCanvas -from View.SedimentLayers.translate import table_headers, retranslate +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.SedimentLayers.translate import SedimentTranslate from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() -class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="SedimentLayersList", study=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers - self.setup_title(title) +class SedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "SedimentLayersList" + _pamhyr_name = "Sediment Layers List" + + def __init__(self, study=None, config=None, parent=None): + self._sediment_layers = study.river.sediment_layers + + name = ( + self._pamhyr_name + " - " + study.name + ) super(SedimentLayersWindow, self).__init__( - name=self._title, ui="SedimentLayersList", parent=parent + title=name, + study=study, + config=config, + trad=SedimentTranslate(), + parent=parent ) - self.setup_sc() self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " + self._study.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): - retranslate() - table = self.find(QTableView, f"tableView") self._table = TableModel( - study = self._study, - undo = self._undo_stack, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "comment"], + data=self._study.river.sediment_layers, + trad=self._trad, + undo=self._undo_stack, ) table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): - self.canvas = MplCanvas(width=5, height=4, dpi=100) + def setup_plot(self): + self.canvas = MplCanvas(width=2, height=4, dpi=100) self.canvas.setObjectName("canvas") self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) @@ -92,12 +99,8 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) - self.find(QAction, "action_edit").triggered.connect(self.edit_sediment_layers) - - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) + self.find(QAction, "action_edit").triggered.connect( + self.edit_sediment_layers) table = self.find(QTableView, f"tableView") table.selectionModel()\ @@ -114,14 +117,14 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): return self.plot = Plot( - canvas = self.canvas, - data = self._sediment_layers.get(rows[0]), - toolbar = None, - display_current = False + canvas=self.canvas, + data=self._sediment_layers.get(rows[0]), + trad=self._trad, + toolbar=None, + display_current=False ) self.plot.draw() - def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -148,16 +151,16 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.delete(rows) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() def edit_sediment_layers(self): @@ -165,8 +168,8 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): for row in rows: slw = EditSedimentLayersWindow( - study = self._study, - sl = self._sediment_layers.get(row), - parent = self + study=self._study, + sl=self._sediment_layers.get(row), + parent=self ) slw.show() diff --git a/src/View/SedimentLayers/translate.py b/src/View/SedimentLayers/translate.py index 29053e17..83a6169d 100644 --- a/src/View/SedimentLayers/translate.py +++ b/src/View/SedimentLayers/translate.py @@ -2,13 +2,16 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - "comment": _translate("SedimentLayers", "Comment"), -} -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["comment"] = _translate("SedimentLayers", "Comment") +class SedimentTranslate(PamhyrTranslate): + def __init__(self): + super(SedimentTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + "comment": _translate("SedimentLayers", "Comment"), + } diff --git a/src/View/SolverParameters/Table.py b/src/View/SolverParameters/Table.py index 5aa488ab..bbf12ada 100644 --- a/src/View/SolverParameters/Table.py +++ b/src/View/SolverParameters/Table.py @@ -34,37 +34,21 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SolverParameters.UndoCommand import * -from View.SolverParameters import translate as tr from Solver.Solvers import solver_long_name, solver_type_list +logger = logging.getLogger() + _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, data=None, undo=None, tab=""): - super(QAbstractTableModel, self).__init__() - self._headers = list(tr.table_headers.keys()) - self._data = data - self._undo = undo - self._tab = tab - self._params = self._data.get_params(self._tab) - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - - column = index.column() - - if self._headers[column] == "value": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._params) - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._tab = self._opt_data + self._lst = self._data.get_params(self._tab) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -75,24 +59,18 @@ class TableModel(QAbstractTableModel): cname = self._headers[column] if cname == "name": - value = self._params.get(row)[cname] - if value in tr.names: - value = tr.names[value] + value = self._lst.get(row)[cname] + if value in self._trad.get_dict("names"): + value = self._trad.get_dict("names")[value] return value elif cname == "value": - value = self._params.get(row)[cname] - if value in tr.yes_no: - value = tr.yes_no[value] + value = self._lst.get(row)[cname] + if value in self._trad.get_dict("yes_no"): + value = self._trad.get_dict("yes_no")[value] return value return QVariant() - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return tr.table_headers[self._headers[section]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -102,12 +80,12 @@ class TableModel(QAbstractTableModel): try: if self._headers[column] == "value": - if value in tr.r_yes_no: - value = tr.r_yes_no[value].lower() + if value in self._trad.get_dict("r_yes_no"): + value = self._trad.get_dict("r_yes_no")[value].lower() self._undo.push( SetCommand( - self._params, row, + self._lst, row, "value", value ) ) @@ -123,7 +101,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # AddCommand( - # self._params, row + # self._lst, row # ) # ) @@ -135,7 +113,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # DelCommand( - # self._params, rows + # self._lst, rows # ) # ) @@ -147,7 +125,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # SortCommand( - # self._params, False + # self._lst, False # ) # ) @@ -164,7 +142,7 @@ class TableModel(QAbstractTableModel): # self._undo_stack.push( # MoveCommand( - # self._params, "up", row + # self._lst, "up", row # ) # ) @@ -172,7 +150,7 @@ class TableModel(QAbstractTableModel): # self.layoutChanged.emit() # def move_down(self, index, parent=QModelIndex()): - # if row > len(self._params): + # if row > len(self._lst): # return # target = row @@ -181,7 +159,7 @@ class TableModel(QAbstractTableModel): # self._undo_stack.push( # MoveCommand( - # self._params, "down", row + # self._lst, "down", row # ) # ) diff --git a/src/View/SolverParameters/UndoCommand.py b/src/View/SolverParameters/UndoCommand.py index d3343d00..2b6804b8 100644 --- a/src/View/SolverParameters/UndoCommand.py +++ b/src/View/SolverParameters/UndoCommand.py @@ -25,6 +25,7 @@ from PyQt5.QtWidgets import ( from Model.SolverParameters.SolverParametersList import SolverParametersList + class SetCommand(QUndoCommand): def __init__(self, data, index, column, new_value): QUndoCommand.__init__(self) diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index c04eebc7..d7100193 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -43,7 +42,7 @@ from PyQt5.QtWidgets import ( from View.SolverParameters.UndoCommand import * from View.SolverParameters.Table import TableModel -from View.SolverParameters import translate as tr +from View.SolverParameters.translate import ParamTranslate from Solver.Solvers import solver_long_name, solver_type_list @@ -51,25 +50,26 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SolverParametersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver parameters", study=None, parent=None): - # Init tanslate dictionary - tr.init() - self._title = title + " - " + study.name +class SolverParametersWindow(PamhyrWindow): + _pamhyr_ui = "SolverParameters" + _pamhyr_name = "Solver parameters" + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(SolverParametersWindow, self).__init__( - name=title, ui="SolverParameters", parent=parent + title=name, + study=study, + config=config, + options=['copy'], + trad=ParamTranslate(), + parent=parent ) - self._study = study self._params = self._study.river.parameters self.setup_sc() self.setup_table() - self.setup_connections() - - self.ui.setWindowTitle(self._title) def setup_sc(self): self._undo_stack = {} @@ -77,10 +77,10 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): for st in solver_type_list: self._undo_stack[st] = QUndoStack() - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self._undo_sc = QShortcut(QKeySequence.Undo, self) + self._redo_sc = QShortcut(QKeySequence.Redo, self) + self._undo_sc.activated.connect(self._undo) + self._redo_sc.activated.connect(self._redo) def setup_table(self): self._table = {} @@ -97,9 +97,13 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): # Create table model self._table[st] = TableModel( - data = self._study.river, - undo = self._undo_stack[st], - tab = st, + table_view=table, + data=self._study.river, + opt_data=st, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["value"], + trad=self._trad, + undo=self._undo_stack[st], ) table.setModel(self._table[st]) @@ -115,31 +119,24 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): # Create tab self._tab_widget.addTab(widget, f"{solver_long_name[st]}") - - def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def current_tab(self): return self.find(QTabWidget, "tabWidget")\ .currentWidget()\ .objectName()\ .replace("tab_", "") - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() self._set_current_reach() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() self._set_current_reach() - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 2508629e..5521402c 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -18,66 +18,118 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = None -yes_no = None -r_yes_no = None -names = None -def init(): - global table_headers - table_headers = { - "name": _translate("SolverParameters", "Name"), - "value": _translate("SolverParameters", "Value") - } +class ParamTranslate(PamhyrTranslate): + def __init__(self): + super(ParamTranslate, self).__init__() - # Used to translate user parameter with value yes or no - global yes_no - yes_no = { - "yes": _translate("SolverParameters", "Yes"), - "no": _translate("SolverParameters", "No"), - "y": _translate("SolverParameters", "Y"), - "n": _translate("SolverParameters", "N"), - } + self._sub_dict["table_headers"] = { + "name": _translate("SolverParameters", "Name"), + "value": _translate("SolverParameters", "Value") + } - # Reverse - global r_yes_no - r_yes_no = { - _translate("SolverParameters", "Yes"): "yes", - _translate("SolverParameters", "No"): "no", - _translate("SolverParameters", "Y"): "y", - _translate("SolverParameters", "N"): "n", - } + self._sub_dict["yes_no"] = { + "yes": _translate("SolverParameters", "Yes"), + "no": _translate("SolverParameters", "No"), + "y": _translate("SolverParameters", "Y"), + "n": _translate("SolverParameters", "N"), + } - global names - names = { - "all_init_time": _translate("SolverParameters", "Initial time (jj:hh:mm:ss)"), - "all_final_time": _translate("SolverParameters", "Final time (jj:hh:mm:ss)"), - "all_timestep": _translate("SolverParameters", "Timestep (second)"), - "all_command_line_arguments": _translate("SolverParameters", "Command line arguments"), - # Mage specific parameters - "mage_min_timestep": _translate("SolverParameters", "Minimum timestep (second)"), - "mage_timestep_tra": _translate("SolverParameters", "Time step of writing on .TRA"), - "mage_timestep_bin": _translate("SolverParameters", "Time step of writing on .BIN"), - "mage_implicitation": _translate("SolverParameters", "Implicitation parameter"), - "mage_continuity_discretization": _translate("SolverParameters", "Continuity discretization type (S/L)"), - "mage_qsj_discretization": _translate("SolverParameters", "QSJ discretization (A/B)"), - "mage_stop_criterion_iterations": _translate("SolverParameters", "Stop criterion iterations (G/A/R)"), - "mage_iteration_type": _translate("SolverParameters", "Iteration type"), - "mage_smooth_coef": _translate("SolverParameters", "Smoothing coefficient"), - "mage_cfl_max": _translate("SolverParameters", "Maximun accepted number of CFL"), - "mage_min_height": _translate("SolverParameters", "Minimum water height (meter)"), - "mage_max_niter": _translate("SolverParameters", "Maximun number of iterations (< 100)"), - "mage_timestep_reduction_factor": _translate("SolverParameters", "Timestep reduction factor"), - "mage_precision_reduction_factor_Z": _translate("SolverParameters", "Reduction precision factor of Z"), - "mage_precision_reduction_factor_Q": _translate("SolverParameters", "Reduction precision factor of Q"), - "mage_precision_reduction_factor_r": _translate("SolverParameters", "Reduction precision factor of residue"), - "mage_niter_max_precision": _translate("SolverParameters", "Number of iteration at maximum precision"), - "mage_niter_before_switch": _translate("SolverParameters", "Number of iteration before switch"), - "mage_max_froude": _translate("SolverParameters", "Maximum accepted Froude number"), - "mage_diffluence_node_height_balance": _translate("SolverParameters", "Diffluence node height balance"), - "mage_compute_reach_volume_balance": _translate("SolverParameters", "Compute reach volume balance (Y/N)"), - "mage_max_reach_volume_balance": _translate("SolverParameters", "Maximum reach volume balance"), - "mage_min_reach_volume_to_check": _translate("SolverParameters", "Minimum reach volume to check"), - } + self._sub_dict["r_yes_no"] = { + _translate("SolverParameters", "Yes"): "yes", + _translate("SolverParameters", "No"): "no", + _translate("SolverParameters", "Y"): "y", + _translate("SolverParameters", "N"): "n", + } + + self._sub_dict["names"] = { + "all_init_time": + _translate("SolverParameters", + "Initial time (jj:hh:mm:ss)"), + "all_final_time": + _translate("SolverParameters", + "Final time (jj:hh:mm:ss)"), + "all_timestep": + _translate("SolverParameters", + "Timestep (second)"), + "all_command_line_arguments": + _translate("SolverParameters", + "Command line arguments"), + # Mage specific parameters + "mage_min_timestep": + _translate("SolverParameters", + "Minimum timestep (second)"), + "mage_timestep_tra": + _translate("SolverParameters", + "Time step of writing on .TRA"), + "mage_timestep_bin": + _translate("SolverParameters", + "Time step of writing on .BIN"), + "mage_implicitation": + _translate("SolverParameters", + "Implicitation parameter"), + "mage_continuity_discretization": + _translate("SolverParameters", + "Continuity discretization type (S/L)"), + "mage_qsj_discretization": + _translate("SolverParameters", + "QSJ discretization (A/B)"), + "mage_stop_criterion_iterations": + _translate("SolverParameters", + "Stop criterion iterations (G/A/R)"), + "mage_iteration_type": + _translate("SolverParameters", + "Iteration type"), + "mage_smooth_coef": + _translate("SolverParameters", + "Smoothing coefficient"), + "mage_cfl_max": + _translate("SolverParameters", + "Maximun accepted number of CFL"), + "mage_min_height": + _translate("SolverParameters", + "Minimum water height (meter)"), + "mage_max_niter": + _translate("SolverParameters", + "Maximun number of iterations (< 100)"), + "mage_timestep_reduction_factor": + _translate("SolverParameters", + "Timestep reduction factor"), + "mage_precision_reduction_factor_Z": + _translate("SolverParameters", + "Reduction precision factor of Z"), + "mage_precision_reduction_factor_Q": + _translate("SolverParameters", + "Reduction precision factor of Q"), + "mage_precision_reduction_factor_r": + _translate("SolverParameters", + "Reduction precision factor of residue"), + "mage_niter_max_precision": + _translate("SolverParameters", + "Number of iteration at maximum precision"), + "mage_niter_before_switch": + _translate("SolverParameters", + "Number of iteration before switch"), + "mage_max_froude": + _translate("SolverParameters", + "Maximum accepted Froude number"), + "mage_diffluence_node_height_balance": + _translate("SolverParameters", + "Diffluence node height balance"), + "mage_compute_reach_volume_balance": + _translate("SolverParameters", + "Compute reach volume balance (Y/N)"), + "mage_max_reach_volume_balance": + _translate("SolverParameters", + "Maximum reach volume balance"), + "mage_min_reach_volume_to_check": + _translate("SolverParameters", + "Minimum reach volume to check"), + "mage_init_internal": + _translate("SolverParameters", + "Use Mage internal initialization (Y/N)"), + } diff --git a/src/View/Stricklers/Table.py b/src/View/Stricklers/Table.py index 58700803..7b740b91 100644 --- a/src/View/Stricklers/Table.py +++ b/src/View/Stricklers/Table.py @@ -46,6 +46,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: diff --git a/src/View/Stricklers/UndoCommand.py b/src/View/Stricklers/UndoCommand.py index 52988b26..b543b046 100644 --- a/src/View/Stricklers/UndoCommand.py +++ b/src/View/Stricklers/UndoCommand.py @@ -24,7 +24,10 @@ from PyQt5.QtWidgets import ( ) from Model.LateralContribution.LateralContribution import LateralContribution -from Model.LateralContribution.LateralContributionList import LateralContributionList +from Model.LateralContribution.LateralContributionList import ( + LateralContributionList +) + class SetNameCommand(QUndoCommand): def __init__(self, data, index, new_value): @@ -41,6 +44,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._data.get(self._index).name = self._new + class SetCommentCommand(QUndoCommand): def __init__(self, data, index, new_value): QUndoCommand.__init__(self) @@ -56,6 +60,7 @@ class SetCommentCommand(QUndoCommand): def redo(self): self._data.get(self._index).comment = self._new + class SetMinorCommand(QUndoCommand): def __init__(self, data, index, new_value): QUndoCommand.__init__(self) @@ -71,6 +76,7 @@ class SetMinorCommand(QUndoCommand): def redo(self): self._data.get(self._index).minor = self._new + class SetMediumCommand(QUndoCommand): def __init__(self, data, index, new_value): QUndoCommand.__init__(self) @@ -104,6 +110,7 @@ class AddCommand(QUndoCommand): else: self._data.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, data, rows): QUndoCommand.__init__(self) @@ -123,6 +130,7 @@ class DelCommand(QUndoCommand): def redo(self): self._data.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, data, _reverse): QUndoCommand.__init__(self) @@ -150,6 +158,7 @@ class SortCommand(QUndoCommand): ) self._old = None + class PasteCommand(QUndoCommand): def __init__(self, data, row, el): QUndoCommand.__init__(self) diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index 12620c4f..b647154c 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -44,40 +43,32 @@ from Model.Stricklers.Stricklers import Stricklers from View.Stricklers.UndoCommand import PasteCommand from View.Stricklers.Table import TableModel -from View.Stricklers.translate import table_headers, retranslate +from View.Stricklers.translate import StricklersTranslate _translate = QCoreApplication.translate logger = logging.getLogger() -class StricklersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Stricklers", study=None, config=None, parent=None): - self._title = title + " - " + study.name + +class StricklersWindow(PamhyrWindow): + _pamhyr_ui = "Stricklers" + _pamhyr_name = "Stricklers" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(StricklersWindow, self).__init__( - name=title, ui="Stricklers", parent=parent + title=name, + study=study, + config=config, + trad=StricklersTranslate(), + parent=parent ) - self._study = study - self._config = config - - self.setup_sc() self.setup_table() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_table(self): - retranslate() self._table = {} for t in ["app", "study"]: @@ -88,11 +79,11 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): data = self._config.stricklers self._table[t] = TableModel( - table_view = table, - table_headers = table_headers, - editable_headers = ["name", "comment", "minor", "medium"], - data = data, - undo = self._undo_stack, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "comment", "minor", "medium"], + data=data, + undo=self._undo_stack, ) table.setModel(self._table[t]) @@ -105,11 +96,6 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def index_selected_rows(self): table = self.find(QTableView, f"tableView_study") return list( @@ -121,7 +107,6 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): ) ) - def add(self): rows = self.index_selected_rows() if len(rows) == 0: @@ -139,14 +124,14 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): def sort(self): self._table['study'].sort(False) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table['study'].undo() - def redo(self): + def _redo(self): self._table['study'].redo() diff --git a/src/View/Stricklers/translate.py b/src/View/Stricklers/translate.py index 9102ac6d..489094cf 100644 --- a/src/View/Stricklers/translate.py +++ b/src/View/Stricklers/translate.py @@ -18,21 +18,18 @@ from PyQt5.QtCore import QCoreApplication -from Model.LateralContribution.LateralContributionTypes import ( - NotDefined, LateralContrib, Rain, Evaporation, -) +from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate -table_headers = { - "name": _translate("LateralContribution", "Name"), - "minor": _translate("LateralContribution", "Minor bed"), - "medium": _translate("LateralContribution", "Medium bed"), - "comment": _translate("LateralContribution", "Comment"), -} -def retranslate(): - table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["minor"] = _translate("LateralContribution", "Minor bed") - table_headers["medium"] = _translate("LateralContribution", "Medium bed") - table_headers["comment"] = _translate("LateralContribution", "Comment") +class StricklersTranslate(PamhyrTranslate): + def __init__(self): + super(StricklersTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("LateralContribution", "Name"), + "minor": _translate("LateralContribution", "Minor bed"), + "medium": _translate("LateralContribution", "Medium bed"), + "comment": _translate("LateralContribution", "Comment"), + } diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py index 7218f9d3..bfb615d1 100644 --- a/src/View/Study/Window.py +++ b/src/View/Study/Window.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from Model.Study import Study -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtCore import QCoreApplication @@ -27,27 +27,39 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate -class NewStudyWindow(ASubWindow): - def __init__(self, study=None, title="New Study", parent=None): - super(NewStudyWindow, self).__init__(name=title, ui="NewStudy", parent=parent) - self.ui.setWindowTitle(title) - self.parent = parent - self.study = study +class NewStudyWindow(PamhyrDialog): + _pamhyr_ui = "NewStudy" + _pamhyr_name = "Edit/New Study" - if not self.study is None: - self.set_line_edit_text("lineEdit_name", self.study.name) - self.set_text_edit_text("textEdit_description", self.study.description) - self.set_datetime_edit("dateTimeEdit_date", self.study.date) + def __init__(self, study=None, config=None, parent=None): + if study is not None: + name = f"Edit study - {study.name}" + else: + name = "New study" + + super(NewStudyWindow, self).__init__( + title=name, + study=study, + config=config, + options=[], + parent=parent + ) + + if self._study is not None: + self.set_line_edit_text("lineEdit_name", self._study.name) + self.set_text_edit_text( + "textEdit_description", self._study.description) + self.set_datetime_edit("dateTimeEdit_date", self._study.date) self.find(QLabel, "label_creation_date_data").setText( - self.study.creation_date.isoformat(sep=" ") + self._study.creation_date.isoformat(sep=" ") ) self.find(QLabel, "label_last_modification_data").setText( - self.study.last_save_date.isoformat(sep=" ") + self._study.last_save_date.isoformat(sep=" ") ) - if self.study.time_system == "date": + if self._study.time_system == "date": self.set_radio_button("radioButton_date", True) self.find(QLabel, "label_date").setEnabled(True) self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True) @@ -71,23 +83,23 @@ class NewStudyWindow(ASubWindow): self.find(QLabel, "label_date").setEnabled(True) self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True) - def accept(self): name = self.get_line_edit_text("lineEdit_name") description = self.get_text_edit_text("textEdit_description") - if self.study is None: + if self._study is None: study = Study.new(name, description) if self.get_radio_button("radioButton_date"): date = self.get_datetime_edit("dateTimeEdit_date") study.use_date(date) self.parent.set_model(study) else: - self.study.name = name - self.study.description = description + self._study.name = name + self._study.description = description if self.get_radio_button("radioButton_date"): date = self.get_datetime_edit("dateTimeEdit_date") - self.study.use_date(date) + self._study.use_date(date) else: - self.study.use_time() + self._study.use_time() + self.done(True) diff --git a/src/View/ASubWindow.py b/src/View/Tools/ASubWindow.py similarity index 83% rename from src/View/ASubWindow.py rename to src/View/Tools/ASubWindow.py index 4e1c1327..7ef5f964 100644 --- a/src/View/ASubWindow.py +++ b/src/View/Tools/ASubWindow.py @@ -46,6 +46,7 @@ from Model.Except import ClipboardFormatError logger = logging.getLogger() + class WindowToolKit(object): def __init__(self, parent=None): super(WindowToolKit, self).__init__() @@ -67,10 +68,16 @@ class WindowToolKit(object): header = [] values = [] + delimiter = '\t' + if ';' in data: + delimiter = ';' + if ' ' in data: + delimiter = ' ' + stream = StringIO(data) - rows = csv.reader(stream, delimiter='\t') - for l, row in enumerate(rows): - if has_header and l == 0: + rows = csv.reader(stream, delimiter=delimiter) + for ind, row in enumerate(rows): + if has_header and ind == 0: header = row.copy() continue @@ -78,7 +85,9 @@ class WindowToolKit(object): return header, values - def file_dialog(self, select_file=True, callback=lambda x: None, directory=None): + def file_dialog(self, select_file=True, + callback=lambda x: None, + directory=None): """Open a new file dialog and send result to callback function Args: @@ -150,7 +159,7 @@ class ASubWindowFeatures(object): return qtype - def get_label_text(self, name:str): + def get_label_text(self, name: str): """Get text of label component Args: @@ -161,7 +170,7 @@ class ASubWindowFeatures(object): """ return self.find(QLabel, name).text() - def set_label_text(self, name:str, text:str): + def set_label_text(self, name: str, text: str): """Set text of label component Args: @@ -173,8 +182,7 @@ class ASubWindowFeatures(object): """ self.find(QLabel, name).setText(text) - - def set_line_edit_text(self, name:str, text:str): + def set_line_edit_text(self, name: str, text: str): """Set text of line edit component Args: @@ -189,7 +197,7 @@ class ASubWindowFeatures(object): except AttributeError as e: logger.error(e) - def get_line_edit_text(self, name:str): + def get_line_edit_text(self, name: str): """Get text of line edit component Args: @@ -200,7 +208,7 @@ class ASubWindowFeatures(object): """ return self.find(QLineEdit, name).text() - def set_text_edit_text(self, name:str, text:str): + def set_text_edit_text(self, name: str, text: str): """Set text of text edit component Args: @@ -212,7 +220,7 @@ class ASubWindowFeatures(object): """ self.find(QTextEdit, name).setText(text) - def get_text_edit_text(self, name:str): + def get_text_edit_text(self, name: str): """Get text of text edit component Args: @@ -223,7 +231,7 @@ class ASubWindowFeatures(object): """ return self.find(QTextEdit, name).toHtml() - def set_plaintext_edit_text(self, name:str, text:str): + def set_plaintext_edit_text(self, name: str, text: str): """Set text of text edit component Args: @@ -235,7 +243,7 @@ class ASubWindowFeatures(object): """ self.find(QPlainTextEdit, name).setPlainText(text) - def get_plaintext_edit_text(self, name:str): + def get_plaintext_edit_text(self, name: str): """Get text of text edit component Args: @@ -246,7 +254,7 @@ class ASubWindowFeatures(object): """ return self.find(QPlainTextEdit, name).toPlainText() - def set_check_box(self, name:str, checked:bool): + def set_check_box(self, name: str, checked: bool): """Set status of checkbox component Args: @@ -258,7 +266,7 @@ class ASubWindowFeatures(object): """ self.find(QCheckBox, name).setChecked(checked) - def get_check_box(self, name:str): + def get_check_box(self, name: str): """Get status of checkbox component Args: @@ -269,8 +277,7 @@ class ASubWindowFeatures(object): """ return self.find(QCheckBox, name).isChecked() - - def set_time_edit(self, name:str, time:str): + def set_time_edit(self, name: str, time: str): """Set time of timeedit component Args: @@ -283,7 +290,7 @@ class ASubWindowFeatures(object): qtime = QTime.fromString(time) self.find(QTimeEdit, name).setTime(qtime) - def get_time_edit(self, name:str): + def get_time_edit(self, name: str): """Get time of timeedit component Args: @@ -294,7 +301,7 @@ class ASubWindowFeatures(object): """ return self.find(QTimeEdit, name).time().toString() - def set_spin_box(self, name:str, value:int): + def set_spin_box(self, name: str, value: int): """Set value of spinbox component Args: @@ -306,7 +313,7 @@ class ASubWindowFeatures(object): """ self.find(QSpinBox, name).setValue(value) - def get_spin_box(self, name:str): + def get_spin_box(self, name: str): """Get time of spin box component Args: @@ -317,7 +324,7 @@ class ASubWindowFeatures(object): """ return self.find(QSpinBox, name).value() - def set_action_checkable(self, name:str, checked:bool): + def set_action_checkable(self, name: str, checked: bool): """Set value of action Args: @@ -329,7 +336,7 @@ class ASubWindowFeatures(object): """ self.find(QAction, name).setChecked(checked) - def get_action_checkable(self, name:str): + def get_action_checkable(self, name: str): """Get status of action Args: @@ -340,8 +347,7 @@ class ASubWindowFeatures(object): """ return self.find(QAction, name).isChecked() - - def set_push_button_checkable(self, name:str, checked:bool): + def set_push_button_checkable(self, name: str, checked: bool): """Set value of push button component Args: @@ -353,7 +359,7 @@ class ASubWindowFeatures(object): """ self.find(QPushButton, name).setChecked(checked) - def get_push_button_checkable(self, name:str): + def get_push_button_checkable(self, name: str): """Get status of push button Args: @@ -364,7 +370,7 @@ class ASubWindowFeatures(object): """ return self.find(QPushButton, name).isChecked() - def set_radio_button(self, name:str, checked:bool): + def set_radio_button(self, name: str, checked: bool): """Set value of radio button component Args: @@ -376,7 +382,7 @@ class ASubWindowFeatures(object): """ self.find(QRadioButton, name).setChecked(checked) - def get_radio_button(self, name:str): + def get_radio_button(self, name: str): """Get status of radio button Args: @@ -387,7 +393,7 @@ class ASubWindowFeatures(object): """ return self.find(QRadioButton, name).isChecked() - def combobox_add_item(self, name:str, item:str): + def combobox_add_item(self, name: str, item: str): """Add item in combo box Args: @@ -399,7 +405,7 @@ class ASubWindowFeatures(object): """ self.find(QComboBox, name).addItem(item) - def combobox_add_items(self, name:str, items:str): + def combobox_add_items(self, name: str, items: str): """Add item in combo box Args: @@ -412,7 +418,7 @@ class ASubWindowFeatures(object): for item in items: self.find(QComboBox, name).addItem(item) - def set_combobox_text(self, name:str, item:str): + def set_combobox_text(self, name: str, item: str): """Set current text of combo box Args: @@ -424,7 +430,7 @@ class ASubWindowFeatures(object): """ self.find(QComboBox, name).setCurrentText(item) - def get_combobox_text(self, name:str): + def get_combobox_text(self, name: str): """Get current text of combo box Args: @@ -435,7 +441,7 @@ class ASubWindowFeatures(object): """ return self.find(QComboBox, name).currentText() - def get_datetime_edit(self, name:str): + def get_datetime_edit(self, name: str): """Get datetime of datetime edit Args: @@ -446,7 +452,7 @@ class ASubWindowFeatures(object): """ return self.find(QDateTimeEdit, name).dateTime().toPyDateTime() - def set_datetime_edit(self, name:str, date:datetime): + def set_datetime_edit(self, name: str, date: datetime): """Set datetime of a datetime edit Args: @@ -463,11 +469,16 @@ class ASubWindowFeatures(object): # Top level interface class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): - def __init__(self, name="", ui="dummy", parent=None): + def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubMainWindow, self).__init__(parent=parent) if ui is not None: self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", f"{ui}.ui" + ) + ), self ) @@ -478,7 +489,7 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): def closeEvent(self, event): if self.parent is not None: - self.parent.sub_win_del(self.name) + self.parent.sub_win_del(self.hash()) def find(self, qtype, name): """Find an ui component @@ -495,11 +506,17 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): return self.ui.findChild(qtype, name) + class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): - def __init__(self, name="", ui="dummy", parent=None): + def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubWindow, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", f"{ui}.ui" + ) + ), self ) self.name = name @@ -509,7 +526,7 @@ class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): def closeEvent(self, event): if self.parent is not None: - self.parent.sub_win_del(self.name) + self.parent.sub_win_del(self.hash()) def find(self, qtype, name): """Find an ui component @@ -526,11 +543,17 @@ class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): return self.ui.findChild(qtype, name) + class AWidget(QWidget, ASubWindowFeatures): def __init__(self, ui="", parent=None): super(AWidget, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", "Widgets", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", "Widgets", f"{ui}.ui" + ) + ), self ) self.parent = parent diff --git a/src/View/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py similarity index 52% rename from src/View/ListedSubWindow.py rename to src/View/Tools/ListedSubWindow.py index b4dfe416..d0b833b8 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/Tools/ListedSubWindow.py @@ -21,8 +21,9 @@ from functools import reduce logger = logging.getLogger() + class ListedSubWindow(object): - def __init__(self): + def __init__(self, **kwargs): super(ListedSubWindow, self).__init__() self.sub_win_cnt = 0 self.sub_win_list = [] @@ -36,62 +37,65 @@ class ListedSubWindow(object): def sub_win_add(self, name, win): self.sub_win_list.append((name, win)) self.sub_win_cnt += 1 - logger.info(f"Open window: {name} ({self.sub_win_cnt})") + try: + logger.info( + f"Open window: {name}: {self.sub_win_cnt}") + except Exception: + logger.info(f"Open window: {name}: {self.sub_win_cnt}: X") + logger.warning(f"Sub window without hash method !") - def sub_win_del(self, name): + def sub_win_del(self, h): self.sub_win_list = list( filter( - lambda x: x[0] != name, + lambda x: x[1].hash() != h, self.sub_win_list ) ) self.sub_win_cnt = len(self.sub_win_list) - logger.info(f"Close window: {name} ({self.sub_win_cnt})") + logger.info(f"Close window: ({h}) {self.sub_win_cnt}") - def _sub_win_exists(self, name): - return reduce( - lambda acc, n: (acc or (n[0] == name)), + def _sub_win_exists(self, h): + res = reduce( + lambda acc, el: (acc or (h == (el[1].hash()))), self.sub_win_list, False ) + return res - def _sub_win_exists_with_contain(self, name, contain): - return reduce( - lambda acc, n: ( - acc or - ( - (n[0] == name) and - reduce( - lambda acc, c: acc and (c in n[1]._title), - contain, - True - ) - ) - ), - self.sub_win_list, - False - ) + def sub_win_exists(self, h): + return self._sub_win_exists(h) - def sub_win_exists(self, name, contain = []): - if contain == []: - return self._sub_win_exists(name) - else: - return self._sub_win_exists_with_contain(name, contain) - - def sub_win_filter_first(self, name, contain): + def get_sub_win(self, h): try: return next( filter( - lambda n: ( - (name in n[0]) and - reduce( - lambda acc, c: acc and (c in n[1]._title), - contain, - True - ) - ), + lambda el: (h == el[1].hash()), self.sub_win_list, ) )[1] - except: + except Exception: return None + + def sub_window_exists(self, cls, + data=None): + """Check if window already exists + + Check if window already exists, used to deni window open + duplication + + Args: + cls: Window class, must inerit to PamhyrWindow or + PamhyrDialog + data: Data used for hash computation of cls + + Returns: + The window if hash already exists on sub window dictionary, + otherelse None + """ + hash = cls._hash(data) + if self.sub_win_exists(hash): + win = self.get_sub_win(hash) + win.activateWindow() + return True + else: + return False diff --git a/src/View/Tools/PamhyrDelegate.py b/src/View/Tools/PamhyrDelegate.py index c5e97da3..9d68c908 100644 --- a/src/View/Tools/PamhyrDelegate.py +++ b/src/View/Tools/PamhyrDelegate.py @@ -32,6 +32,7 @@ from View.Tools.PamhyrWidget import ( logger = logging.getLogger() + class PamhyrExTimeDelegate(QItemDelegate): def __init__(self, data=None, mode="time", parent=None): super(PamhyrExTimeDelegate, self).__init__(parent) @@ -61,8 +62,9 @@ class PamhyrExTimeDelegate(QItemDelegate): def updateEditorGeometry(self, editor, option, index): r = QRect(option.rect) - if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: - r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) editor.setGeometry(r) @pyqtSlot() diff --git a/src/View/Tools/PamhyrPlot.py b/src/View/Tools/PamhyrPlot.py new file mode 100644 index 00000000..600687d5 --- /dev/null +++ b/src/View/Tools/PamhyrPlot.py @@ -0,0 +1,44 @@ +# PamhyrPlot.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from View.Tools.Plot.APlot import APlot +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + + +class PamhyrPlot(APlot): + def __init__(self, data=None, trad=None, + canvas=None, toolbar=None, + parent=None): + if canvas is None: + canvas = MplCanvas() + + self._trad = trad + self._canvas = canvas + self._toolbar = toolbar + self._parent = parent + + super(PamhyrPlot, self).__init__(data=data) + + @property + def canvas(self): + return self._canvas + + @property + def toolbar(self): + return self._toolbar diff --git a/src/View/Tools/PamhyrPythonEditor.py b/src/View/Tools/PamhyrPythonEditor.py new file mode 100644 index 00000000..2cc1680c --- /dev/null +++ b/src/View/Tools/PamhyrPythonEditor.py @@ -0,0 +1,42 @@ +# PamhyrPythonEditor.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from PyQt5.QtGui import QFont, QFontMetrics, QColor +from PyQt5.Qsci import QsciScintilla, QsciLexerPython + +logger = logging.getLogger() + + +class PamhyrPythonEditor(QsciScintilla): + def __init__(self, parent=None): + super(PamhyrPythonEditor, self).__init__(parent) + + # Set the default font + font = QFont() + font.setFamily('Monospace') + font.setFixedPitch(True) + font.setPointSize(10) + self.setFont(font) + self.setMarginsFont(font) + + # Set Python lexer + lexer = QsciLexerPython() + lexer.setDefaultFont(font) + self.setLexer(lexer) diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 623ebd3b..803e5842 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, - QComboBox, QStyledItemDelegate, + QComboBox, QStyledItemDelegate, QHeaderView, ) logger = logging.getLogger() @@ -57,6 +57,7 @@ class PamhyrTextDelegate(QStyledItemDelegate): def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) + class PamhyrTableModel(QAbstractTableModel): def _setup_delegates(self): if self._table_view is None: @@ -78,10 +79,12 @@ class PamhyrTableModel(QAbstractTableModel): table_view=None, table_headers={}, editable_headers=[], - delegates = {}, + delegates={}, + trad=None, data=None, undo=None, - opt_data=None): + opt_data=None, + parent=None): super(PamhyrTableModel, self).__init__() self._table_view = table_view @@ -90,6 +93,8 @@ class PamhyrTableModel(QAbstractTableModel): self._headers = list(table_headers.keys()) self._editable_headers = editable_headers self._delegates = delegates + self._trad = trad + self._parent = parent self._data = data self._opt_data = opt_data @@ -98,30 +103,39 @@ class PamhyrTableModel(QAbstractTableModel): self._setup_delegates() self._setup_lst() + self._table_view_configure() def _setup_lst(self): self._lst = self._data + def _table_view_configure(self): + self._table_view.setModel(self) + self._table_view.setSelectionBehavior(QAbstractItemView.SelectRows) + self._table_view.horizontalHeader()\ + .setSectionResizeMode(QHeaderView.Stretch) + self._table_view.setAlternatingRowColors(True) + self._table_view.resizeColumnsToContents() + def flags(self, index): column = index.column() options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if (self._editable_headers or - self._headers[column] in self._editable_headers): + if self._headers[column] in self._editable_headers: options |= Qt.ItemIsEditable return options - def rowCount(self, parent): + def rowCount(self, parent=QModelIndex()): return len(self._lst) - def columnCount(self, parent): + def columnCount(self, parent=QModelIndex()): return len(self._headers) def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return self._table_headers[self._headers[section]] + if role == Qt.ItemDataRole.DisplayRole: + if orientation == Qt.Orientation.Horizontal: + return self._table_headers[self._headers[section]] return QVariant() @@ -138,3 +152,6 @@ class PamhyrTableModel(QAbstractTableModel): def redo(self): self._undo.redo() self.layoutChanged.emit() + + def update(self): + self.layoutChanged.emit() diff --git a/src/View/Tools/PamhyrTranslate.py b/src/View/Tools/PamhyrTranslate.py new file mode 100644 index 00000000..67befa23 --- /dev/null +++ b/src/View/Tools/PamhyrTranslate.py @@ -0,0 +1,58 @@ +# PamhyrTranslate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + + +class PamhyrTranslate(object): + def __init__(self): + # Module global dictionnary + self._dict = {} + # Module sub dictionnary + self._sub_dict = {} + + self._dict["pamhyr"] = _translate("Pamhyr", "Pamhyr2") + + def __getitem__(self, index): + if index not in self._dict: + return None + return self._dict[index] + + def _get_in_sub_dict(self, dictionary, index): + if dictionary not in self._sub_dict: + return None + return self._sub_dict[index] + + def get(self, dictionary, index): + if dictionary == "global": + return self[index] + + return self._get_in_sub_dict(dictionary, index) + + def get_dict(self, dictionary): + _dict = None + + if dictionary == "global": + _dict = self._dict + + if dictionary in self._sub_dict: + _dict = self._sub_dict[dictionary] + + return _dict diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index 60e06174..d3e02dd9 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -18,6 +18,8 @@ import logging +from datetime import datetime, timedelta + from PyQt5.QtCore import ( QModelIndex, QRect, QTime, QDateTime, ) @@ -26,10 +28,22 @@ from PyQt5.QtWidgets import ( QSpinBox, QTimeEdit, QDateTimeEdit, QItemDelegate, ) -from View.ASubWindow import AWidget +from View.Tools.ASubWindow import AWidget logger = logging.getLogger() + +class PamhyrWidget(AWidget): + _pamhyr_ui = None + _pamhyr_name = "PamhyrWidget" + + def __init__(self, parent=None): + super(PamhyrWidget, self).__init__( + ui=self._pamhyr_ui, + parent=parent + ) + + class PamhyrExtendedTimeEdit(AWidget): def __init__(self, parent=None): super(PamhyrExtendedTimeEdit, self).__init__( @@ -70,6 +84,7 @@ class PamhyrExtendedTimeEdit(AWidget): return timedelta(days=days, seconds=secs) + class PamhyrExtendedDateTimeEdit(AWidget): def __init__(self, parent=None): super(ExtendedDateTimeEdit, self).__init__( diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py new file mode 100644 index 00000000..87829b8d --- /dev/null +++ b/src/View/Tools/PamhyrWindow.py @@ -0,0 +1,193 @@ +# PamhyrWindow.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtWidgets import ( + QUndoStack, QShortcut, +) + +from View.Tools.ASubWindow import ASubMainWindow, ASubWindow +from View.Tools.ListedSubWindow import ListedSubWindow + +logger = logging.getLogger() + + +class PamhyrWindowTools(object): + def __init__(self, options=["undo", "copy"], parent=None, **kwargs): + super(PamhyrWindowTools, self).__init__() + + self._hash_data = [] + self._undo_stack = None + + if "undo" in options: + self._init_undo() + + if "copy" in options: + self._init_copy() + + # Undo/Redo + + def _init_undo(self): + self._undo_stack = QUndoStack() + + self._undo_sc = QShortcut(QKeySequence.Undo, self) + self._redo_sc = QShortcut(QKeySequence.Redo, self) + + self._undo_sc.activated.connect(self._undo) + self._redo_sc.activated.connect(self._redo) + + def _undo(self): + if self._undo_stack is not None: + self._undo_stack.undo() + self._update() + + def _redo(self): + if self._undo_stack is not None: + self._undo_stack.redo() + self._update() + + # Copy/Paste + + def _init_copy(self): + self._copy_sc = QShortcut(QKeySequence.Copy, self) + self._paste_sc = QShortcut(QKeySequence.Paste, self) + + self._copy_sc.activated.connect(self._copy) + self._paste_sc.activated.connect(self._paste) + + def _copy(self): + if self._copy_stack is not None: + self._copy_stack.copy() + self._update() + + def _paste(self): + if self._copy_stack is not None: + self._copy_stack.redo() + self._update() + + # Display + + def _set_title(self): + """Apply `self._title` at current window title displayed + + Returns: + Nothing + """ + self.ui.setWindowTitle(self._title) + + def _update(self): + """Update window display component + + Returns: + Nothing + """ + self._set_title() + + # Hash methods + + @classmethod + def _hash(cls, data): + """Compute window hash + + Args: + data: window data parameters + + Returns: + The hash + """ + hash_str = cls._pamhyr_name + hash_str += cls._pamhyr_ui + + for el in data: + hash_str += repr(el) + + h = hash(hash_str) + + return h + + def hash(self): + """Compute window hash + + Returns: + The hash + """ + return self._hash(self._hash_data) + + +class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): + _pamhyr_ui = "dummy" + _pamhyr_name = "PamhyrWindow" + + def __init__(self, + title="Pamhyr2", + study=None, config=None, trad=None, + options=["undo", "copy"], + parent=None): + self._title = title + self._study = study + self._config = config + self._trad = trad + self._parent = parent + + super(PamhyrWindow, self).__init__( + name=self._pamhyr_name, + ui=self._pamhyr_ui, + parent=parent, + ) + + self._hash_data.append(self._study) + self._hash_data.append(self._config) + + self._set_title() + + +class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): + _pamhyr_ui = "dummy" + _pamhyr_name = "PamhyrWindow" + + def __init__(self, + title="Pamhyr2", + study=None, config=None, trad=None, + options=["undo", "copy"], + parent=None): + self._title = title + self._study = study + self._config = config + self._trad = trad + self._parent = parent + + super(PamhyrDialog, self).__init__( + name=self._pamhyr_name, + ui=self._pamhyr_ui, + parent=parent, + ) + + self._hash_data.append(self._study) + self._hash_data.append(self._config) + + self._set_title() + + def done(self, result): + if self.parent is not None: + self.parent.sub_win_del(self.hash()) + super(PamhyrDialog, self).done(result) diff --git a/src/View/Plot/APlot.py b/src/View/Tools/Plot/APlot.py similarity index 82% rename from src/View/Plot/APlot.py rename to src/View/Tools/Plot/APlot.py index a863f1e6..439ae2e9 100644 --- a/src/View/Plot/APlot.py +++ b/src/View/Tools/Plot/APlot.py @@ -18,27 +18,21 @@ from Model.Except import NotImplementedMethodeError + class APlot(object): - def __init__(self, canvas=None, data=None, toolbar=None): + def __init__(self, data=None): super(APlot, self).__init__() self._init = False - - self._canvas = canvas self._data = data - self._toolbar = toolbar - - @property - def canvas(self): - return self._canvas @property def data(self): return self._data - @property - def toolbar(self): - return self._toolbar + @data.setter + def data(self, data): + self._data = data def draw(self): """Draw plot @@ -48,8 +42,7 @@ class APlot(object): """ raise NotImplementedMethodeError(self, self.draw) - - def update(self, ind = None): + def update(self, ind=None): """Update plot Update the plot, update for data index IND if define, else diff --git a/src/View/Plot/mpl_canvas_onpick_event.py b/src/View/Tools/Plot/OnPickEvent.py similarity index 84% rename from src/View/Plot/mpl_canvas_onpick_event.py rename to src/View/Tools/Plot/OnPickEvent.py index fdfa1de6..87ab0758 100644 --- a/src/View/Plot/mpl_canvas_onpick_event.py +++ b/src/View/Tools/Plot/OnPickEvent.py @@ -26,6 +26,7 @@ from shapely.geometry.polygon import Polygon as ShapelyPolygon logger = logging.getLogger() + class OnpickEvent(object): def __init__(self, ax, x, y, x_carto, y_carto, tableview=None): """ @@ -74,8 +75,11 @@ class OnpickEvent(object): def select_row_pt_clicked(self, ind: int = 0): """ Args: - ind: Indice de la ligne où se trouve le point le plus proche 'visé'. - Returns: Sélectionne la ligne (du tableau) correspondant au point le plus proche 'visé' à la suite de l'événement onpick. + ind: Indice de la ligne où se trouve le point le plus proche + 'visé'. + + Returns: Sélectionne la ligne (du tableau) correspondant au point le + plus proche 'visé' à la suite de l'événement onpick. """ if self.tableView is not None: selectionModel = self.tableView.selectionModel() @@ -97,7 +101,8 @@ class OnpickEvent(object): def select_point_plot(self): """ - Returns: sélectionne le(s) point(s) du graphique correspondant à la/aux ligne(s) sélectionnée(s) dans le tableau. + Returns: sélectionne le(s) point(s) du graphique correspondant à + la/aux ligne(s) sélectionnée(s) dans le tableau. """ if self.tableView is not None: rows = list(set( @@ -117,20 +122,24 @@ class OnpickEvent(object): self.pt1 = self.ax.plot(self.x[row], self.y[row], '+', c='Blue', markersize=7) self.pt.append(self.pt1) - self.update_select_point_point_bis(self.x[row], self.y[row]) + self.update_select_point_point_bis( + self.x[row], self.y[row]) elif len(rows) == 1: for row in rows: try: [pl[0].set_data([], []) for pl in self.pt if len(self.pt) > 1] - except: + except Exception: logger.info("update_select_point_point: Update issue") try: self.update_select_point_point_bis(self.x[row], self.y[row]) - except: - logger.info("update_select_point_point_bis: Update issue, possible index missing") + except Exception: + logger.info( + "update_select_point_point_bis: Update issue, " + + "possible index missing" + ) self.ax.figure.canvas.draw_idle() @@ -147,13 +156,18 @@ class OnpickEvent(object): def plot_selection_point(self, x, y): """ Args: - x: abscisse - y: ordonnée - Returns: sélectionne le point du graphique correspond à la ligne sélectionnée dans le tableau. + x: abscissa + y: ordinate + + Returns: sélectionne le point du graphique correspond à la ligne + sélectionnée dans le tableau. """ if self.tableView is not None: - self.select_point, = self.ax.plot(x, y, - '+', c='Blue', markersize=7) + self.select_point, = self.ax.plot( + x, y, + '+', c='Blue', + markersize=7 + ) else: self.select_point, = self.ax.plot([], []) @@ -207,7 +221,8 @@ class OnpickEvent(object): @property def delta_x(self): """" - Returns: la longueur entre les limites de la vue sur l'axe des x, c'est-à-dire |x_max_visible - x_min_visible|. + Returns: la longueur entre les limites de la vue sur l'axe des x, + c'est-à-dire |x_max_visible - x_min_visible|. """ xgauche, xdroite = self.ax.get_xlim() delta_x = abs(xdroite - xgauche) @@ -216,7 +231,8 @@ class OnpickEvent(object): @property def delta_y(self): """ - Returns: la longueur entre les limites de la vue sur l'axe des y, c'est à dire |y_max_visible - y_min_visible|. + Returns: la longueur entre les limites de la vue sur l'axe des y, + c'est à dire |y_max_visible - y_min_visible|. """ ybas, yhaut = self.ax.get_ylim() delta_y = abs(yhaut - ybas) @@ -226,8 +242,9 @@ class OnpickEvent(object): def indice_points_onpick(event): """ Args: event - Returns: le(s) indexe(s) du/des point(s) (plus précisement les coordonnées de points) capturé(s) - par l'événement onpick (voir picker) + Returns: le(s) indexe(s) du/des point(s) (plus précisement les + coordonnées de points) capturé(s) par l'événement onpick + (voir picker) """ return event.ind @@ -235,9 +252,9 @@ class OnpickEvent(object): """ Args: event: - Returns: une array contenant les coordonées des points qui se trouvent dans la zone définie par l'événement - onpick (voir picker) - + Returns: une array contenant les coordonées des points qui se + trouvent dans la zone définie par l'événement onpick + (voir picker) """ thisline = event.artist xdata = thisline.get_xdata() @@ -251,8 +268,9 @@ class OnpickEvent(object): """ Args: event: - Returns: la liste des distances normées (en m) entre les points situés dans la région définie par l'événement - onpick (voir picker). + Returns: la liste des distances normées (en m) entre les points + situés dans la région définie par l'événement onpick + (voir picker). """ ind = event.ind thisline = event.artist @@ -279,15 +297,23 @@ class OnpickEvent(object): """ Args: event: - Returns: la liste des distances 'visuelles' entre les points situés dans la région définie par l'événement - onpick (voir picker). + Returns: la liste des distances 'visuelles' entre les points situés + dans la région définie par l'événement onpick (voir picker). """ bbox = self.ax.get_window_extent()\ .transformed(self.ax.figure.dpi_scale_trans.inverted()) ratio_w_sur_h = bbox.width / bbox.height distances_ecran = [ - (((x - event.mouseevent.xdata) / (self.delta_x * ratio_w_sur_h)) ** 2 + - ((y - event.mouseevent.ydata) / self.delta_y) ** 2) ** (1 / 2) + ( + ( + (x - event.mouseevent.xdata) / + (self.delta_x * ratio_w_sur_h) + ) ** 2 + + ( + (y - event.mouseevent.ydata) / + self.delta_y + ) ** 2 + ) ** (1 / 2) for (x, y) in self.points_onpick(event) ] @@ -295,9 +321,11 @@ class OnpickEvent(object): def distances(self, event): """ - Args: event: - Returns: la liste des distances entre la position de la souris et tous les points se trouvant dans - la zone définie par l'événement onpick ( voir picker) + Args: + event: + Returns: la liste des distances entre la position de la souris et + tous les points se trouvant dans la zone définie par + l'événement onpick (voir picker) """ distances = np.linalg.norm( self.points_onpick(event) - self.position_souris(event), @@ -308,18 +336,23 @@ class OnpickEvent(object): def index_pt_plus_proche_ecran(self, event): """ - Args: - event: - Returns: indice du point le plus proche visuellement de la position du click. - """ + Args: + event: + Returns: indice du point le plus proche visuellement de la position + du click. + """ dataidx_ecran = np.argmin(self.distance_ecran(event)) return dataidx_ecran def point_plus_proche_ecran(self, event): point_onpick = self.points_onpick(event) - datapos_ecran = point_onpick[self.index_pt_plus_proche_ecran(event)] + datapos_ecran = point_onpick[ + self.index_pt_plus_proche_ecran(event) + ] - return self.points_onpick(event)[self.index_pt_plus_proche_ecran(event)] + return self.points_onpick(event)[ + self.index_pt_plus_proche_ecran(event) + ] def index_pt_plus_proche(self, event): """ @@ -382,7 +415,8 @@ class OnpickEvent(object): self.pos_x_annotation = x_min + ((x_max - x_min) / 2) percent = 0 - y_ecran_lim = ((max(self.ax.set_ylim()) - min(self.ax.set_ylim())) / 2) + y_ecran_lim = ((max(self.ax.set_ylim()) - + min(self.ax.set_ylim())) / 2) if abs(y_ecran_lim) > 4: percent = 0.05 @@ -404,8 +438,10 @@ class OnpickEvent(object): cte = event.y * 0.1 / 100 self.y_pos_text_param_hydrau = event.ydata + cte - self.annotation_onclick.set_position((self.pos_x_annotation, - self.y_pos_text_param_hydrau)) + self.annotation_onclick.set_position( + (self.pos_x_annotation, + self.y_pos_text_param_hydrau) + ) self.ax.callbacks.connect('ylim_changed', self.on_ylims_change) @@ -450,7 +486,7 @@ class OnpickEvent(object): try: self.poly_col_bis.remove() self.largeur_au_miroir(event) - except: + except Exception: self.largeur_au_miroir(event) self.annotate_onclick(event) @@ -473,7 +509,8 @@ class OnpickEvent(object): """ Args: val: Valeur de la cote Z à laquelle on veut caluler A , p et L. - Returns: la valeur de la section mouillée A, du périmètre mouillé p et de la largeur au miroir L. + Returns: la valeur de la section mouillée A, du périmètre mouillé p + et de la largeur au miroir L. """ largeur_miroir = 0. section_mouillee_totale = 0. @@ -491,7 +528,10 @@ class OnpickEvent(object): perimetre_shapely = 0. perim_calc = 0. - for polyg, coul in zip(liste_chemins, couleurs[0:len(liste_chemins)]): + for polyg, coul in zip( + liste_chemins, + couleurs[0:len(liste_chemins)] + ): points_polygone = polyg.vertices xs = points_polygone[:, 0] ys = points_polygone[:, 1] @@ -499,13 +539,16 @@ class OnpickEvent(object): liste_points_miroir = [ x for (x, y) in zip(xs, ys) if np.isclose(y, val) ] - largeur_miroir_polygone = liste_points_miroir[-2] - liste_points_miroir[0] + largeur_miroir_polygone = liste_points_miroir[-2] - \ + liste_points_miroir[0] largeur_miroir += largeur_miroir_polygone polygone_shapely = ShapelyPolygon(points_polygone) aire_calculee_shapely = polygone_shapely.area perimetre_shapely = polygone_shapely.length - perimetre_mouille_total_shapely = polygone_shapely.length - largeur_miroir + perimetre_mouille_total_shapely = ( + polygone_shapely.length - largeur_miroir + ) liste_points_fond = [ (x, y) for (x, y) in zip(xs, ys) if not np.isclose(y, val) ] @@ -514,7 +557,9 @@ class OnpickEvent(object): perimetre = 0 aire = 0 - for un_point in liste_points_fond + [(min(liste_points_miroir), val)]: + for un_point in liste_points_fond + [ + (min(liste_points_miroir), val) + ]: x_pt_suivant, y_pt_suivant = un_point perimetre += ((x_pt_prec - x_pt_suivant) ** 2 + (y_pt_prec - y_pt_suivant) ** 2) ** (1 / 2) diff --git a/src/View/Plot/MplCanvas.py b/src/View/Tools/Plot/PamhyrCanvas.py similarity index 96% rename from src/View/Plot/MplCanvas.py rename to src/View/Tools/Plot/PamhyrCanvas.py index ee1f5373..b8962c2e 100644 --- a/src/View/Plot/MplCanvas.py +++ b/src/View/Tools/Plot/PamhyrCanvas.py @@ -32,7 +32,7 @@ class MplCanvas(FigureCanvasQTAgg): '(x = ' + format(x, '1.4f') + ', \t' + ' y = ' + format(y, '1.4f') + ')' ) - self.axes.grid(color='green', linestyle='--', linewidth=0.5) + self.axes.grid(color='black', linestyle='--', linewidth=0.5) self.axes.yaxis.tick_left() self.axes.xaxis.tick_bottom() self.axes.spines[['top', 'right']].set_color('none') diff --git a/src/View/Plot/PamhyrToolbar.py b/src/View/Tools/Plot/PamhyrToolbar.py similarity index 73% rename from src/View/Plot/PamhyrToolbar.py rename to src/View/Tools/Plot/PamhyrToolbar.py index 6a729f04..489d47b1 100644 --- a/src/View/Plot/PamhyrToolbar.py +++ b/src/View/Tools/Plot/PamhyrToolbar.py @@ -39,7 +39,8 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): """ isometric_signal = pyqtSignal(str) - def __init__(self, canvas, parent, items = ["home", "move", "zoom", "save"]): + def __init__(self, canvas, parent, + items=["home", "move", "zoom", "save"]): """PamhyrPlotToolbar Args: @@ -56,12 +57,25 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): icons = [] if "home" in items: - self.toolitems.append(('Home', _translate("Toolbar", 'Default view'), 'home', 'home')) + self.toolitems.append( + ( + 'Home', + _translate("Toolbar", 'Default view'), + 'home', 'home' + ) + ) self.toolitems.append((None, None, None, None)) if "back/forward" in items: - self.toolitems.append(('Back', _translate("Toolbar", 'Back to previous view'), 'back', 'back')) - self.toolitems.append(('Forward', _translate("Toolbar", 'Return to next view'), 'forward', 'forward')) + self.toolitems.append( + ( + 'Back', + _translate("Toolbar", 'Back to previous view'), + 'back', 'back' + ) + ) + self.toolitems.append(('Forward', _translate( + "Toolbar", 'Return to next view'), 'forward', 'forward')) self.toolitems.append((None, None, None, None)) if "move" in items: @@ -73,17 +87,21 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): self.toolitems.append((None, None, None, None)) if "zoom" in items: - self.toolitems.append(('Zoom', _translate("Toolbar", 'Zoom'), 'zoom_to_rect', 'zoom')) + self.toolitems.append( + ( + 'Zoom', + _translate("Toolbar", 'Zoom'), + 'zoom_to_rect', 'zoom' + ) + ) self.toolitems.append((None, None, None, None)) icon_zoom = QtGui.QIcon() icon_zoom.addPixmap(QtGui.QPixmap( - os.path.abspath(f"{file_path}/../ui/ressources/zoom.png") + os.path.abspath(f"{file_path}/../../ui/ressources/zoom.png") )) - logger.info(os.path.abspath(f"{file_path}/../ui/ressources/zoom.png")) - - icons.append(("zoom",icon_zoom)) + icons.append(("zoom", icon_zoom)) if "iso" in items: self.toolitems.append( @@ -102,11 +120,21 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): icon_btn_isometric_view = QtGui.QIcon() icon_btn_isometric_view.addPixmap( - QtGui.QPixmap(os.path.abspath(f"{file_path}/../ui/ressources/zoom_fit_11.png"))) + QtGui.QPixmap( + os.path.abspath( + f"{file_path}/../../ui/ressources/zoom_fit_11.png" + ) + ) + ) icon_btn_global_view = QtGui.QIcon() icon_btn_global_view.addPixmap( - QtGui.QPixmap(os.path.abspath(f"{file_path}/../ui/ressources/zoom_fit.png"))) + QtGui.QPixmap( + os.path.abspath( + f"{file_path}/../../ui/ressources/zoom_fit.png" + ) + ) + ) icons.append(("isometric_view", icon_btn_isometric_view)) icons.append(("non_isometric_view", icon_btn_global_view)) @@ -175,10 +203,16 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): def toolitems_translation(self): self._actions['home'].setToolTip(_translate("Toolbar", "Default view")) - self._actions['back'].setToolTip(_translate("Toolbar", "Back to previous view")) - self._actions['forward'].setToolTip(_translate("Toolbar", "Return to next view")) - self._actions['pan'].setToolTip(_translate("Toolbar", "Axes panoramic")) + self._actions['back'].setToolTip( + _translate("Toolbar", "Back to previous view")) + self._actions['forward'].setToolTip( + _translate("Toolbar", "Return to next view")) + self._actions['pan'].setToolTip( + _translate("Toolbar", "Axes panoramic")) self._actions['zoom'].setToolTip(_translate("Toolbar", "Zoom")) - self._actions['save_figure'].setToolTip(_translate("Toolbar", "Save the figure")) - self.action_isometric_view.setToolTip(_translate("Toolbar", "Isometric view (Shift+W)")) - self.action_auto_global_view.setToolTip(_translate("Toolbar", "Auto scale view (Shift+X)")) + self._actions['save_figure'].setToolTip( + _translate("Toolbar", "Save the figure")) + self.action_isometric_view.setToolTip( + _translate("Toolbar", "Isometric view (Shift+W)")) + self.action_auto_global_view.setToolTip( + _translate("Toolbar", "Auto scale view (Shift+X)")) diff --git a/src/View/ui/BasicHydraulicStructures.ui b/src/View/ui/BasicHydraulicStructures.ui new file mode 100644 index 00000000..add59b7c --- /dev/null +++ b/src/View/ui/BasicHydraulicStructures.ui @@ -0,0 +1,142 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + Basic hydraulic structures + + + + + + + Qt::Horizontal + + + + + + + + 300 + 0 + + + + + 0 + 0 + + + + + + + + 5 + + + + + Enable / Disable basic hydraulic structure + + + true + + + + + + + + + + Qt::Vertical + + + + + + + + + + + + + + + + + + + + 0 + 0 + 800 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add + + + Add a new point + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete + + + Delete points + + + + + + ressources/edit.pngressources/edit.png + + + Edit + + + Edit selected hydraulic structure + + + + + + diff --git a/src/View/ui/BoundaryConditions.ui b/src/View/ui/BoundaryConditions.ui index ac32ee3e..82064977 100644 --- a/src/View/ui/BoundaryConditions.ui +++ b/src/View/ui/BoundaryConditions.ui @@ -114,7 +114,7 @@ Add - Add a new boundary condition or lateral contribution + Add a new boundary condition or punctual contribution Ctrl+N @@ -144,7 +144,7 @@ Edit - Edit boundary condition or lateral contribution + Edit boundary condition or punctual contribution Ctrl+E diff --git a/src/View/ui/CustomPlotValuesSelectionDialog.ui b/src/View/ui/CustomPlotValuesSelectionDialog.ui new file mode 100644 index 00000000..88ca363c --- /dev/null +++ b/src/View/ui/CustomPlotValuesSelectionDialog.ui @@ -0,0 +1,93 @@ + + + Dialog + + + + 0 + 0 + 414 + 482 + + + + Dialog + + + + + + Qt::Horizontal + + + + + + + X axis: + + + + + + + + + + + Y axis: + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/View/ui/DebugRepl.ui b/src/View/ui/DebugRepl.ui index 03494a0c..c060c1c0 100644 --- a/src/View/ui/DebugRepl.ui +++ b/src/View/ui/DebugRepl.ui @@ -20,9 +20,6 @@ - - - @@ -44,6 +41,11 @@ + + + Monospace + + true diff --git a/src/View/ui/EditBoundaryConditions.ui b/src/View/ui/EditBoundaryConditions.ui index 7f615964..101befa6 100644 --- a/src/View/ui/EditBoundaryConditions.ui +++ b/src/View/ui/EditBoundaryConditions.ui @@ -86,7 +86,7 @@ Add - Add a new point in boundary condition or lateral contribution + Add a new point in boundary condition or punctual contribution Ctrl+N @@ -116,7 +116,7 @@ Sort - Sort boundary condition point + Sort boundary condition points diff --git a/src/View/ui/GeometryCrossSection.ui b/src/View/ui/GeometryCrossSection.ui new file mode 100644 index 00000000..6e6d62a5 --- /dev/null +++ b/src/View/ui/GeometryCrossSection.ui @@ -0,0 +1,137 @@ + + + MainWindow + + + + 0 + 0 + 1120 + 630 + + + + MainWindow + + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 1120 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + add + + + Add a point on cross-section + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + delete + + + Delete selected point(s) + + + + + + ressources/up.pngressources/up.png + + + up + + + Move up selected point(s) + + + + + + ressources/down.pngressources/down.png + + + down + + + Mode down selected point(s) + + + + + + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png + + + sort_asc + + + Sort points by nearest neighbor + + + + + + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png + + + sort_des + + + Sort reversed points by nearest neighbor + + + + + + diff --git a/src/View/ui/GeometryReach.ui b/src/View/ui/GeometryReach.ui new file mode 100644 index 00000000..d18d0e14 --- /dev/null +++ b/src/View/ui/GeometryReach.ui @@ -0,0 +1,233 @@ + + + MainWindow + + + + 0 + 0 + 1280 + 720 + + + + MainWindow + + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + + + + + ressources/go-up2.pngressources/go-up2.png + + + + + + + + + Qt::Vertical + + + true + + + true + + + + + + + + + + + + + ressources/go-down1.pngressources/go-down1.png + + + + + + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1280 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + + + import + + + Import geometry + + + + + export + + + Export geometry + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + add + + + Add a cross-section + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + delete + + + Delete selected cross-section(s) + + + + + + ressources/edit.pngressources/edit.png + + + edit + + + Edit selected cross section(s) + + + + + + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png + + + sort_asc + + + Sort cross-sections by ascending KP + + + + + + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png + + + sort_des + + + Sort cross-sections by descending KP + + + + + + ressources/up.pngressources/up.png + + + up + + + Move up selected cross-section(s) + + + + + + ressources/down.pngressources/down.png + + + down + + + Move down selected cross-section(s) + + + + + + diff --git a/src/View/ui/HydraulicStructures.ui b/src/View/ui/HydraulicStructures.ui new file mode 100644 index 00000000..813da5b5 --- /dev/null +++ b/src/View/ui/HydraulicStructures.ui @@ -0,0 +1,139 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + Hydraulic structures + + + + + + + Qt::Horizontal + + + + + + + + 300 + 0 + + + + + 0 + 0 + + + + + + + + 5 + + + + + Enable / Disable hydraulic structure + + + true + + + + + + + + + + Qt::Vertical + + + + + + + + + + + + + + + + 0 + 0 + 800 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add + + + Add a new point + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete + + + Delete points + + + + + + ressources/edit.pngressources/edit.png + + + Edit + + + Edit selected hydraulic structure + + + + + + diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index ea77d698..bf73c3d3 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -82,9 +82,7 @@ &File - - @@ -92,9 +90,7 @@ - - @@ -113,7 +109,6 @@ &Geometry - @@ -123,9 +118,7 @@ &Execute - - @@ -135,18 +128,17 @@ &Hydraulics - - + + - + - &Plots + &Results - - + @@ -161,8 +153,23 @@ &Help - - + + + Help + + + + Pamhyr2 + + + + + + + + + + @@ -178,7 +185,7 @@ - + @@ -195,6 +202,7 @@ + Sans Serif 50 false true @@ -233,37 +241,14 @@ - + - - - - - Sans Serif - 9 - 50 - false - false - - - - toolBar_2 - - - TopToolBarArea - - - true - - - - @@ -362,6 +347,9 @@ Save + + Save study + Ctrl+S @@ -374,6 +362,9 @@ Save as ... + + Save study as ... + Ctrl+Shift+S @@ -396,6 +387,9 @@ Quit + + Quit application + Ctrl+F4 @@ -497,13 +491,10 @@ - Boundary conditions and one-time contributions + Boundary conditions and punctual contributions - - 75 - true - + @@ -597,19 +588,18 @@ Close - + false - Hydrograph + Visualize last results + + + Visualize the last results - - Ubuntu - 75 - true - + @@ -676,7 +666,7 @@ Save current study - Enrégistrer étude en cours (Ctrl+S) + Save the study (Ctrl+S) @@ -691,7 +681,7 @@ Close current study - Fermer étude en cours (Ctrl+F) + Close the study (Ctrl+F) Ctrl+F @@ -810,7 +800,7 @@ Boundary conditions - Edit boundary conditions and one-time contributions + Edit boundary conditions and punctual contributions @@ -865,7 +855,7 @@ Edit study - Edit study + Edit the study metadata @@ -890,12 +880,69 @@ Sediment layers + + Define sediment layers + Edit reach sediment layers + + + Doc + + + + + Pamhyr2 developer + + + + + Mage + + + Open mage documentation + + + + + Users (pdf) + + + + + Users (html) + + + + + Developers (pdf) + + + + + Developers (html) + + + + + Reservoirs + + + Edit reservoirs + + + + + Hydraulic structures + + + Edit hydraulic structures + + diff --git a/src/View/ui/ProfileSedimentLayers.ui b/src/View/ui/ProfileSedimentLayers.ui index b9f5106d..05f6618e 100644 --- a/src/View/ui/ProfileSedimentLayers.ui +++ b/src/View/ui/ProfileSedimentLayers.ui @@ -39,20 +39,6 @@ - - - toolBar - - - TopToolBarArea - - - false - - - - - diff --git a/src/View/ui/Reservoir.ui b/src/View/ui/Reservoir.ui new file mode 100644 index 00000000..d2dc3902 --- /dev/null +++ b/src/View/ui/Reservoir.ui @@ -0,0 +1,120 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + 300 + 0 + + + + + 0 + 0 + + + + + + + + + + + + + + 0 + 0 + 800 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add + + + Add a new point + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete + + + Delete points + + + + + + ressources/edit.pngressources/edit.png + + + Edit + + + Edit elevation/surface law + + + + + + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png + + + Sort + + + Sort points by elevation + + + + + + diff --git a/src/View/ui/ReservoirList.ui b/src/View/ui/ReservoirList.ui new file mode 100644 index 00000000..db3b822d --- /dev/null +++ b/src/View/ui/ReservoirList.ui @@ -0,0 +1,114 @@ + + + MainWindow + + + + 0 + 0 + 849 + 600 + + + + + 300 + 0 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + 300 + 0 + + + + + + + + + + + + + + + + + 0 + 0 + 849 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add + + + Add a new reservoir + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete + + + Delete reservoirs + + + + + + ressources/edit.pngressources/edit.png + + + Edit + + + Edit reservoir law + + + + + + diff --git a/src/View/ui/Results.ui b/src/View/ui/Results.ui index 5b797724..7ff3f594 100644 --- a/src/View/ui/Results.ui +++ b/src/View/ui/Results.ui @@ -6,8 +6,8 @@ 0 0 - 942 - 655 + 1280 + 720 @@ -17,6 +17,12 @@ + + + 874 + 0 + + @@ -28,7 +34,26 @@ Qt::Vertical - + + + + + + + + + Qt::Vertical + + + true + + + true + + + + + @@ -43,25 +68,29 @@ - - - Qt::Vertical - - - - Qt::Horizontal - - - + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + + + + + + + + - - - - - - - - + + @@ -103,25 +132,90 @@ - - - - Qt::Vertical - - - true - - - true - - - - - - Qt::Horizontal - - + + + + + + + + + ressources/media-skip-backward.pngressources/media-skip-backward.png + + + + + + + + + + + ressources/media-seek-backward.pngressources/media-seek-backward.png + + + + + + + true + + + true + + + Qt::DefaultContextMenu + + + true + + + + + + + ressources/media-playback-start.png + ressources/media-playback-pause.pngressources/media-playback-start.png + + + true + + + true + + + + + + + + + + + ressources/media-seek-forward.pngressources/media-seek-forward.png + + + + + + + + + + + ressources/media-skip-forward.pngressources/media-skip-forward.png + + + + + + + Qt::Horizontal + + + + @@ -134,12 +228,42 @@ 0 0 - 942 + 1280 22 + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add + + + Add custom visualization + + + + + Reload + + diff --git a/src/View/ui/SolverLog.ui b/src/View/ui/SolverLog.ui index c94c57db..7f9ce7b1 100644 --- a/src/View/ui/SolverLog.ui +++ b/src/View/ui/SolverLog.ui @@ -22,6 +22,9 @@ Monospace + + + diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui new file mode 100644 index 00000000..225c5948 --- /dev/null +++ b/src/View/ui/WebView.ui @@ -0,0 +1,80 @@ + + + MainWindow + + + + 0 + 0 + 800 + 450 + + + + MainWindow + + + + + + + + + + + + + + + 0 + 0 + 800 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + Open + + + Open document in Firefox + + + + + + ressources/gtk-go-back.pngressources/gtk-go-back.png + + + back + + + + + + ressources/gtk-go-forward.pngressources/gtk-go-forward.png + + + forward + + + + + + diff --git a/src/View/ui/ressources/media-playback-pause.png b/src/View/ui/ressources/media-playback-pause.png new file mode 100644 index 00000000..d1d8541d Binary files /dev/null and b/src/View/ui/ressources/media-playback-pause.png differ diff --git a/src/View/ui/ressources/media-playback-start.png b/src/View/ui/ressources/media-playback-start.png new file mode 100644 index 00000000..4b696d8e Binary files /dev/null and b/src/View/ui/ressources/media-playback-start.png differ diff --git a/src/View/ui/ressources/media-seek-backward.png b/src/View/ui/ressources/media-seek-backward.png new file mode 100644 index 00000000..05bff6fc Binary files /dev/null and b/src/View/ui/ressources/media-seek-backward.png differ diff --git a/src/View/ui/ressources/media-seek-forward.png b/src/View/ui/ressources/media-seek-forward.png new file mode 100644 index 00000000..9610f752 Binary files /dev/null and b/src/View/ui/ressources/media-seek-forward.png differ diff --git a/src/View/ui/ressources/media-skip-backward.png b/src/View/ui/ressources/media-skip-backward.png new file mode 100644 index 00000000..8f9480c7 Binary files /dev/null and b/src/View/ui/ressources/media-skip-backward.png differ diff --git a/src/View/ui/ressources/media-skip-forward.png b/src/View/ui/ressources/media-skip-forward.png new file mode 100644 index 00000000..c2ab904e Binary files /dev/null and b/src/View/ui/ressources/media-skip-forward.png differ diff --git a/src/config.py b/src/config.py index b3ddf6b4..e5c314c3 100644 --- a/src/config.py +++ b/src/config.py @@ -32,23 +32,26 @@ config_file = "config.sqlite3" logger = logging.getLogger() + class Config(SQL): def __init__(self): - self._version = '0.0.3' + self._version = '0.0.4' self.filename = Config.filename() self.set_default_value() logging.info(f"Configuration file : {self.filename}") - super(Config, self).__init__(filename = self.filename) + super(Config, self).__init__(filename=self.filename) def _create(self): # Info (meta data) - self.execute("CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") + self.execute( + "CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") self.execute(f"INSERT INTO info VALUES ('version', '{self._version}')") # Key / Value (data) - self.execute("CREATE TABLE data(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") + self.execute( + "CREATE TABLE data(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") # Solver self.execute(""" @@ -79,12 +82,15 @@ class Config(SQL): self.commit() - def _update(self): - version = self.execute(f"SELECT value FROM info WHERE key='version'")[0] + version = self.execute( + f"SELECT value FROM info WHERE key='version'")[0] if version != self._version: - logger.info(f"Configuration file update from {version} to {self._version}...") + logger.info( + "Configuration file update from " + + f"{version} to {self._version}..." + ) major, minor, release = version.strip().split(".") @@ -92,6 +98,8 @@ class Config(SQL): if int(release) < 2: # Add default solver posix = os.name == 'posix' + ext = "" if posix else ".exe" + self.execute(f""" INSERT INTO solver VALUES ( 'mage8', @@ -101,13 +109,20 @@ class Config(SQL): '', '', '', '', - '@install_dir/mage/mage{"" if posix else ".exe"} @args @input', + '@install_dir/mage/mage{ext} @args @input', '' ) """) + if int(release) < 3: self.execute(f"INSERT INTO data VALUES ('last_study', '')") - self.execute(f"INSERT INTO data VALUES ('close_correctly', 'True')") + self.execute( + f"INSERT INTO data VALUES ('close_correctly', 'True')") + + if int(release) < 4: + self.execute( + f"INSERT INTO data VALUES ('last_solver_name', '')" + ) self.commit() @@ -116,7 +131,7 @@ class Config(SQL): solvers = self.execute( "SELECT * FROM solver", - fetch_one = False + fetch_one=False ) for solver in solvers: solver_type = solver[0] @@ -139,7 +154,7 @@ class Config(SQL): id = 0 stricklers = self.execute( "SELECT * FROM stricklers", - fetch_one = False + fetch_one=False ) for strickler in stricklers: new = Stricklers() @@ -186,6 +201,10 @@ class Config(SQL): v = self.execute("SELECT value FROM data WHERE key='close_correctly'") self.close_correctly = v[0] == "True" + # Last Solver + v = self.execute("SELECT value FROM data WHERE key='last_solver_name'") + self.last_solver_name = v[0] + # Debug v = self.execute("SELECT value FROM data WHERE key='debug'") self.debug = v[0] == "True" @@ -200,17 +219,17 @@ class Config(SQL): self.execute(f""" INSERT INTO solver VALUES ( '{solver._type}', - '{self._sql_format(solver._name)}', - '{self._sql_format(solver._description)}', - '{self._sql_format(solver._path_input)}', - '{self._sql_format(solver._path_solver)}', - '{self._sql_format(solver._path_output)}', - '{self._sql_format(solver._cmd_input)}', - '{self._sql_format(solver._cmd_solver)}', - '{self._sql_format(solver._cmd_output)}' + '{self._db_format(solver._name)}', + '{self._db_format(solver._description)}', + '{self._db_format(solver._path_input)}', + '{self._db_format(solver._path_solver)}', + '{self._db_format(solver._path_output)}', + '{self._db_format(solver._cmd_input)}', + '{self._db_format(solver._cmd_solver)}', + '{self._db_format(solver._cmd_output)}' ) """, - commit = True) + commit=True) def _save_stricklers(self): self.execute(f"DELETE FROM stricklers") @@ -218,8 +237,8 @@ class Config(SQL): for stricklers in self.stricklers.stricklers: self.execute(f""" INSERT INTO stricklers VALUES ( - '{self._sql_format(stricklers._name)}', - '{self._sql_format(stricklers._comment)}', + '{self._db_format(stricklers._name)}', + '{self._db_format(stricklers._comment)}', '{stricklers._minor}', '{stricklers._medium}' ) @@ -241,16 +260,17 @@ class Config(SQL): "last_study": self.last_study, "close_correctly": self.close_correctly, "debug": self.debug, + "last_solver_name": self.last_solver_name, } for key in data: self.execute( f"INSERT OR IGNORE INTO data VALUES " + - f" ('{key}', '{self._sql_format(data[key])}')" + f" ('{key}', '{self._db_format(data[key])}')" ) self.execute( f"UPDATE data SET " + - f"value='{self._sql_format(data[key])}' " + + f"value='{self._db_format(data[key])}' " + f"WHERE key='{key}'" ) @@ -263,10 +283,12 @@ class Config(SQL): self._solvers = [] posix = os.name == 'posix' + ext = "" if posix else ".exe" + ctor = solver_type_list["mage8"] new = ctor("default-mage") new._description = "Default Pamhyr2 mage 8 version" - new._cmd_solver = f""""@install_dir/mage/mage{"" if posix else ".exe"}" @args @input""" + new._cmd_solver = f""""@install_dir/mage/mage{ext}" @args @input""" self._solvers.append(new) # Meshing tool @@ -295,21 +317,43 @@ class Config(SQL): self.last_study = "" self.close_correctly = False + # Last Solver + self.last_solver_name = "" + # Debug self.debug = False def set_close_correctly(self): self.close_correctly = True - self.execute(f"UPDATE data SET value='True' WHERE key='close_correctly'") + self.execute( + f"UPDATE data SET value='True' WHERE key='close_correctly'") self.commit() def set_last_study(self, filename): + if filename is None: + return + self.last_study = filename self.close_correctly = False - self.execute(f"UPDATE data SET value='{self._sql_format(self.last_study)}' WHERE key='last_study'") - self.execute(f"UPDATE data SET value='{self.close_correctly}' WHERE key='close_correctly'") + self.execute( + "UPDATE data SET " + + f"value='{self._db_format(self.last_study)}' " + + "WHERE key='last_study'" + ) + self.execute( + f"UPDATE data SET value='{self.close_correctly}' " + + "WHERE key='close_correctly'" + ) self.commit() + def update_last_solver_used(self, solver_name): + self.last_solver_name = solver_name + self.execute( + "UPDATE data SET " + + f"value='{self._db_format(self.last_solver_name)}' " + + "WHERE key='last_solver_name'" + ) + @classmethod def filename(cls): file = "" @@ -320,7 +364,7 @@ class Config(SQL): ndir = os.path.expanduser('~') + config_dir ndir = ndir.replace("/", "\\") - os.makedirs(ndir, exist_ok = True) + os.makedirs(ndir, exist_ok=True) file = ndir + config_file return file diff --git a/src/init.py b/src/init.py index 0d602128..390725c7 100644 --- a/src/init.py +++ b/src/init.py @@ -16,11 +16,12 @@ # -*- coding: utf-8 -*- -import sys, os +import sys +import os import locale import logging -from PyQt5.QtCore import QTranslator +from PyQt5.QtCore import QTranslator, QLibraryInfo from tools import ( reset_timers, display_timers, timer, @@ -53,21 +54,23 @@ try: handler.setFormatter(formatter) handler.setLevel(logging.DEBUG) logger.addHandler(handler) -except: - logger.error("Failed to create logfile...") +except Exception as e: + logger.error(f"Failed to create logfile... {e}") -def license(): +def legal_info(): "Display in stdout Pamhyr infomation about licence and version" - blue = lambda s: logger.info(f"{logger_color_blue()}{s}{logger_color_reset()}") + def blue(s): return logger.info( + f"{logger_color_blue()}{s}{logger_color_reset()}") - blue("""`7MM\"""Mq. db `7MMM. ,MMF'`7MMF' `7MMF'`YMM' `MM'`7MM\"""Mq.""") - blue(""" MM `MM. ;MM: MMMb dPMM MM MM VMA ,V MM `MM.""") - blue(""" MM ,M9 ,V^MM. M YM ,M MM MM MM VMA ,V MM ,M9 pd*"*b.""") - blue(""" MMmmdM9 ,M `MM M Mb M' MM MMmmmmmmMM VMMP MMmmdM9 (O) j8""") - blue(""" MM AbmmmqMA M YM.P' MM MM MM MM MM YM. ,;j9""") - blue(""" MM A' VML M `YM' MM MM MM MM MM `Mb. ,-='""") - blue(""".JMML. .AMA. .AMMA..JML. `' .JMML..JMML. .JMML. .JMML. .JMML. .JMM. Ammmmmmm""") + with open(os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "motd.txt" + ) + ), "r") as f: + for line in f: + blue(line.rstrip()) with open(os.path.abspath( os.path.join( @@ -76,14 +79,31 @@ def license(): ) ), "r") as f: version = f.readline().strip() - logger.info(f"version: {logger_color_green()}{version}{logger_color_reset()}") + logger.info( + f"version: {logger_color_green()}{version}{logger_color_reset()}") logger.info("license: pamhyr Copyright (C) 2023 INRAE") logger.info("license: This program comes with ABSOLUTELY NO WARRANTY.") - logger.info("license: This is free software, and you are welcome to redistribute it") + logger.info( + "license: This is free software," + + " and you are welcome to redistribute it" + ) logger.info("license: under certain conditions.") -def setup_lang(app, conf:Config): + +def debug_info(): + py_version = sys.version.split("\n")[0] + logger.debug(f"python: {py_version}") + + info = QLibraryInfo + logger.debug(f"Qt PrefixPath: {info.location(QLibraryInfo.PrefixPath)}") + logger.debug( + "Qt LibExecPath: " + + f"{info.location(QLibraryInfo.LibraryExecutablesPath)}" + ) + + +def setup_lang(app, conf: Config): "Return QTranslator configured for current language" lang_file = "" translator = QTranslator() diff --git a/src/lang/fr.ts b/src/lang/fr.ts index 1042a5a5..2efedd5c 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -1,125 +1,154 @@ - + + About - + Contributors: Contributeurs : + + + About + A propos + BoundaryCondition - + Not associate Non associer - + X - + Y - + Time Temps - + Date Date - + Z (m) Z (m) - + days jours - + day jour - + Not defined Non définie - + Ponctual contribution Contributions ponctuelles - + Name Nom - + Type Type - + Node Nœud - Discharge (m³/s) + Discharge (m³/s) Débit (m³/s) - + Solid (kg/s) Solide (kg/s) - + Solid Solide TimeZ - TempsHauteur + TempsHauteur TimeDischarge - TempsDébit + TempsDébit ZDischarge - HauteurDébit + HauteurDébit - - - Discharge (m³/s) - + + + Z(t) + Z(t) + + + + Q(t) + Q(t) + + + + Q(Z) + Q(Z) + + + + CheckList + + + Name + Nom + + + + Status + Status Checker - + Mage network graph {mode} checker Vérificateur {mode} graph reseaux hydraulique pour Mage - + Check if the network graph is valid Vérifie si le graph réseau est valide @@ -134,40 +163,86 @@ Vérifie si il exists au moins un Bief dans l'étude - + Study geometry checker Vérificateur de géometrie de l'étude - + Check if exists geometry for each reach of study Vérifie si la géométrie exists pour chaque bief de l'étude - + Dummy ok - + Dummy warning - + Dummy error + + Configure + + + Name + Nom + + + + Type + Type + + + + Description + Description + + + + CustomPlot + + + Time (sec) + Temps (s) + + + + Kp (m) + Pk (m) + + + + Elevation (m) + Altitude (m) + + + + Water elevation (m) + Cote (m) + + + + Discharge (m³/s) + Débit (m³/s) + + Dialog - + Dialog - + Name Nom @@ -177,7 +252,7 @@ Type - + Description Description @@ -371,96 +446,106 @@ Discharge Débit (m³/s) + + + X axis: + Axe X: + + + + Y axis: + Axe Y: + Exception - + Generic error message Message d'erreur générique - + Undefined error message Message d'érreur non définie - + Method not implemented Méthode non implémenter - + Method Méthode - + not implemented Non implémenter - + for class pour classe - + Not implemented method Méthode non implémenter - + FileFormatError ErreurFormatDeFichier - + Invalid file format: Format de fichier invalide : - + File format error Erreur de format de fichier - + Invalid file format Format de fichier invalide - + Invalid file Fichier invalide - + format because of - + format à cause de - + Clipboard format error - + Erreur de format dans le presse-papier - + without header - + Sans en-tête - + with header - + Avec en-tête - + Invalid clipboard data format: Format des données du presse-papier invalide : - + Clipboard format unknown Presse-papier format inconnu @@ -468,14 +553,14 @@ Form - + Form - + Formulaire dd/MM/yyyy HH:mm:ss - + jj/MM:aaaa HH:mm:ss @@ -485,7 +570,7 @@ HH:mm:ss - + HH:mm:ss @@ -510,17 +595,17 @@ ... - + ... D50 - + D50 Sigma - + Sigma @@ -531,32 +616,32 @@ Frictions - + Not defined Non définie Name - Nom + Nom - + Begin kp (m) Pk de départ (m) - + End kp (m) Pk de fin (m) - + Begin strickler strickler de départ - + End strickler Strickler de fin @@ -564,168 +649,251 @@ Geometry - + Name Nom Kp (m) - Pk (m) + Pk (m) Type - Type + Type - + upstream amont - + downstream aval + + + X (m) + X (m) + + + + Y (m) + Y (m) + + + + Z (m) + Z (m) + + + + Traversal abs (m) + Absice en travers (m) + + + + Open a file + Ouvrir un fichier + + + + File mage geometry (*.ST) + Fichier géométrie Mage (*.ST) + + + + File mage meshed geometry (*.M) + Fichier géométrie maillée (*.M) + + + + All file (*) + Tous les fichiers (*) + + + + reach + Bief + + + + reachs + Biefs + + + + cross-section + Section en travers + + + + cross-sections + Sections en travers + + + + KP + PK + + + + InitialCondition + + + Elevation (m) + Altitude (m) + + + + Discharge (m³/s) + Débit (m³/s) + + + + KP (m) + PK (m) + + + + Height (m) + Hauteur (m) + LateralContribution - + Name Nom - + Minor bed Lit mineur - + Medium bed Lit moyen - + Comment Commentaire - + Not associate Non associer - + X - + Y - + Time Temps - + Date Date - + Z (m) - + days jours - + day jour - + Not defined Non définie - + Lateral contribution Contribution laterale - + Rain Pluie - + Evaporation Évaporation - + Type Type - + Reach Bief - + Begin kp (m) Pk de départ (m) - + End kp (m) Pk de fin (m) KP (m) - PK (m) + PK (m) Elevation (m) - Altitude (m) + Altitude (m) Height (m) - Hauteur (m) + Hauteur (m) - Discharge (m³/s) - Débit (m³/s) - - - Discharge (m³/s) - + Débit (m³/s) MainWindow - + MainWindow Fenêtre principale - + toolBar Bar d'outils @@ -750,12 +918,12 @@ &Fichier - + &River Network &Réseau - + &Geometry &Géométrie @@ -765,252 +933,247 @@ Comparer - + &Execute &Executer - + &Hydraulics &Hydraulique &Plots - Gra&phique + Gra&phique - + &Cartography &Cartographie - + &Help &Aide - - toolBar_2 - - - - + New study Nouvelle étude - + Ctrl+N Ctrl+N - + Open a study Ouvrir une étude - + Ctrl+O Ctrl+O - + Import data from MAGE Importer des données d'une étude MAGE - + Import data from RubarBE Importer des données d'une étude RubarBE - + Close Fermer - + Close current study Fermer l'étude en cours - + Save mesh Sauvegarder le maillage - + Save Sauvegarder - + Ctrl+S Ctrl+S - + Save as ... Sauvegarder sous... - + Ctrl+Shift+S Ctrl+Shift+S - + Archive Archive - + Pamhyr configuration Configuration de PAMHYR - + Quit Quitter - + Ctrl+F4 Ctrl+F4 - + Edit river network Éditer le réseau - + Edit geometry Éditer la géométrie - + Import geometry Importer une géométrie - + Export geometry Exporter la géométrie - + Run extrenal meshing tool Lancer le mailler externe - + choose meshing tool by reach - + View meshed geometry Voir la géométrie mailler - + Export mesh Exporter le maillage - + Delete mesh of current reach Supprimer le maillage - + Delete all mesh - + Abscisse - Cote - + XYZ - + Numerical parameter for solvers Paramètre numerique des solvers Boundary conditions and one-time contributions - Condition aux limites et apports ponctuels + Condition aux limites et apports ponctuels - + Initial conditions Conditions initiales - + Export initial conditions Exporter les conditions initiales - + Import final state as initial condition Importer un état final comme conditions initiales - + Edit friction Éditer les frottements - + Edit lateral contributions Éditer les apports latéraux - + Edit spills - + Edit cross building Éditer les ouvrages - + Run solver Lancer un solver - + F5 F5 - + Stop solver Stopper le solver - + Display listings - + Simulation directory management - + Open Ouvrir @@ -1020,92 +1183,92 @@ Hydrogramme - + Limnigram Limnigramme - + Map current reach Cartographier le bief sélectionné - + Help PAMHYR Aide de PAMHYR - + Help MAGE Aide de MAGE - + About A propos - + ouvrir - + Save current study Sauvegarder l'étude - + Ctrl+F Ctrl+F - + Quit application - Quitter l'application + Quitter l'application - + Ctrl+Q Ctrl+Q - + stop solver - + Interrompt la simulation en cours - + Ctrl+C Ctrl+C - + Run external meshing tool Lancer le mailler externe - + Run meshing tool on current reach geometry Lancer le mailler externe sur le bief selectionné - + Display simulation listing - + Display current simulation listing - + River network Réseau @@ -1115,82 +1278,82 @@ Géométrie - + Edit reach geometry Éditer la géométrie - + Mesh Maillage - + Boundary conditions Conditions aux limites - + Edit boundary conditions and one-time contributions Éditer les conditions aux limites et les apports ponctuels - + Lateral contribution Contributions latérales - + Edit lateral contribution Éditer les contributions latérales - + Spills - + Edit lateral spills - + Friction Frottements - + Stricklers - + Edit the study stricklers Éditer les Stricklers de l'étude - + Building Ouvrages - + Edit building (valve, ...), singularity and pump Éditer les ouvrages - + Edit study Éditer l'étude - + English - + French @@ -1205,12 +1368,12 @@ Ouvrir dans l'éditeur - + Eval Évaluer - + Ctrl+Return Ctrl+Return @@ -1230,7 +1393,7 @@ Suspenssion - + Add Ajouter @@ -1240,22 +1403,22 @@ Ajouter une condition aux limites ou un apport ponctuel - + Delete Supprimer - + Delete current selected rows Supprimer les lignes selectionnées - + Ctrl+D Ctrl+D - + Edit Éditer @@ -1265,12 +1428,12 @@ Éditer une condition aux limites ou un apport ponctuel - + Ctrl+E Ctrl+E - + Sort Trier @@ -1305,7 +1468,7 @@ Trier les stricklers - + delete Supprimer @@ -1335,22 +1498,22 @@ Ressayer la vérification - + Stop Stopper - + Start Commencer - + Pause Pause - + LogFile Fichier de log @@ -1395,32 +1558,32 @@ Trier les points des conditions aux limites - + Quit the application (Ctrl+Q) Quitter l'application (Ctrl+Q) - + Run a solver Lancer un solver - + Display meshed reach - + Edit friction frictions Éditer les frottements - + Define initial conditions Définire les conditions initiales - + results Résultats @@ -1440,17 +1603,17 @@ - + &Sediment &Sédiments - + Sediment layers Couche sédimentaires - + Edit reach sediment layers Éditer les couches sédimentaires @@ -1480,7 +1643,7 @@ Sédiment - + Edit sediment layers list Éditer la liste des couches sédimentaires @@ -1500,595 +1663,595 @@ Éditer les couches sédimentaires du profile - + Add sediment layers Ajouter des couches - + Add specific sediment layers on selected point(s) - + Delete sediment layers Supprimer couche sédimentaires - + Delete specific sediment layers of selected point(s) - + Edit sediment layers - - + + Édition des Tronçons - - + + Ouvrir une étude - - - Enrégistrer étude en cours (Ctrl+S) + + + &Results - - - Fermer étude en cours (Ctrl+F) + + + Help + + + + + Pamhyr2 + + + + + Save study + + + + + Save study as ... + + + + + Boundary conditions and punctual contributions + Conditions limites et apports ponctuels + + + + Visualize last results + + + + + Visualize the last results + + + + + Save the study (Ctrl+S) + + + + + Close the study (Ctrl+F) + + + + + Edit the study metadata + + + + + Define sediment layers + + + + + Doc + + + + + Pamhyr2 developer + + + + + Mage + + + + + Open mage documentation + + + + + Users (pdf) + + + + + Users (html) + + + + + Developers (pdf) + + + + + Developers (html) + + + + + Reservoirs + Casiers + + + + Edit reservoirs + Éditer les casiers + + + + import + importer + + + + export + exporter + + + + add + ajouter + + + + Add a cross-section + + + + + Delete selected cross-section(s) + + + + + edit + + + + + Edit selected cross section(s) + + + + + sort_asc + + + + + Sort cross-sections by ascending KP + + + + + sort_des + + + + + Sort cross-sections by descending KP + + + + + up + + + + + Move up selected cross-section(s) + + + + + down + + + + + Move down selected cross-section(s) + + + + + Add a new point + + + + + Delete points + + + + + Edit elevation/surface law + + + + + Sort points by elevation + + + + + Add a new reservoir + + + + + Delete reservoirs + + + + + Edit reservoir law + + + + + Open document in Firefox + + + + + back + + + + + forward + + + + + Add a point on cross-section + + + + + Delete selected point(s) + + + + + Move up selected point(s) + + + + + Mode down selected point(s) + + + + + Sort points by nearest neighbor + + + + + Sort reversed points by nearest neighbor + + + + + Add custom visualization + + + + + Reload - + MainWindowProfile - - - Profile - - (no name) - (sans nom) + (sans nom) - - Quittez ? - - - - - MainWindowProfile - - - - - Trier les points par ordre croissant de X - - - - - Trier les points par ordre croissant de Y - - - - - Nom - - - - - Abs en travers (m) - - - - + La cote du fond Z minimale - + La cote maximale Z maximale - + Rive gauche - + Rive droite - - Abscisse en travers (m) + + Transverse abscissa (m) - - Cote (m) - - - - - Suppression les lignes incomplètes - - - - - Supprimer les lignes des cellules non renseignées ? - - - - - Suppression des noms répétés - - - - - Etes-vous sûr de vouloir quitter ? - - - - - Insérer un point - - - - - Supprimer le/les point(s) sélectionnés - - - - - Trier les points par ordre décroissant de X - - - - - Trier les points par ordre décroissant de Y - - - - - Décaler le point sélectionné vers le haut - - - - - Décaler le point sélectionné vers le bas - - - - - Exporter (dans un fichier) les points du profil au format tabulé - - - - - Copier la sélection au format tabulé - - - - - Coller la sélection depuis le presse-papier au format tabulé - - - - - Vérifier la validité de la saisie et garder ou pas les modifications apportées - - - - - Annuler toutes les modifications depuis la dernière validation - - - - - Annuler toutes les modifications et revenir à l'état initial - - - - - Ligne d'eau : - Z : Cote (m) - A : Aire mouillée (mu00B2) - p : Périmètre mouillé (m) - L : Largeur au miroir (m) - - - - - 'Maj + Clic' : Ligne d'eau & 'Ctrl + Clic' : Sélectionner des points - - - - - Abscisse en travers calculée en projétant les points -sur le plan défini par les deux points nommés extrêmes - + + Height (m) + Hauteur (m) - + MainWindow_reach - + Stricklers Stricklers - + Kp (m) Pk (m) - - Ouvrir un fichier - - - - - Fichiers .ST (*.ST) - - - - - Fichiers .M (*.M) - - - - - Tous les fichiers (*) - - - - - Kp : - - - - - Files .ST(*.ST or *.st) - - - - - All files (*) - - - - - Abscisse en travers (m) - - - - - Cote (m) - - - - + Profil suivant - - - Trier les profils par ordre croissant des Pk - - Name - Nom + Nom Type - Type + Type - - Alt+Z - - - - - Alt+E - - - - - Alt+R - - - - - Vue globale automatique (Alt+S) - - - - - Vue globale automatique (Alt+D) - - - - - Vue globale automatique (Alt+F) - - - - + X (m) - - Y (m) - - - - + KP (m) Discharge (m^3/s) - Débit (m³/s) + Débit (m³/s) - + Elevation (m) Altitude (m) - + Height (m) Hauteur (m) - + Select destination file - + Transverse abscissa (m) - + Previous cross-section - + Cross-section - + Next cross-section - - Geometry reach edit - - - - - Kp = - - - - - Import geometry - - - - - New profile - - - - - Delete selected profile - - - - - Editer selected profile - - - - - Édition des profils sélectionnés - - - - - Vous avez sélectionné plus de 5 profils. -Seuls les 5 premiers seront édités. - - - - - Profil N° : - - - - + Profil précédent - - + + Profil sélectionné - - - Trier les profils par ordre décroissant des Pk - - - - - Changer l'ordre des profils (en décalant le profil sélectionné vers le haut) - - - - - Changer l'ordre des profils (en décalant le profil sélectionné vers le bas) - - - - - Vue isométrique (Alt+Z) - - - - - Vue isométrique (Alt+E) - - - - - Vue isométrique (Alt+R) - - Network - + Add node Ajouter un nœud - + Delete the node Supprimer un nœud - + Disable the node Déactiver un nœud - + Delete the reach Supprimer un bief - + Disable the reach Déactiver un bief - + Enable the reach Activer un bief - + Reverse the reach orientation Inverser l'orientation du bief - + Name Nom - + Type Type - + Source node Nœud source - + Destination node Nœud destination - Results + Pamhyr - - X (m) - + + Pamhyr2 + Pamhyr2 + + + + Reservoir + + + Not associated + Non associé - - Y (m) - - - - - Reach name - Nom du bief - - - + Name Nom - + + Node + Nœud + + + + Elevation (m) + Altitude (m) + + + + Area (hectare) + Aire (hectare) + + + + Results + + + X (m) + X (m) + + + + Y (m) + Y (m) + + + + Reach name + Nom du bief + + + + Name + Nom + + + KP (m) PK (m) - + days jours - + day jour - + Time (s) Temps (s) - - Discharge (m³/s) - Débit (m³/s) - - Discharge (m³/s) - + Débit (m³/s) SedimentLayers - + (no name) (sans nom) - + Name Nom @@ -2098,64 +2261,74 @@ Seuls les 5 premiers seront édités. Type - + (no name - @kp) (sans nom - @kp) - + Not defined Non définie - + X (m) - + X (m) - + Y (m) - + Y (m) - + Sediment layers Couche sédimentaires - + KP (m) PK (m) - + Comment Commentaire - + D50 - + D50 - + Sigma - + Sigma - + Critical constraint Contrainte critique - + Z (m) - + Z (m) - + Height - Altitude + Hauteur + + + + Height (m) + Hauteur (m) + + + + Kp (m) + Pk (m) @@ -2171,32 +2344,32 @@ Seuls les 5 premiers seront édités. Valeur - + Yes Oui - + No Non - + Y O - + N N - + Initial time (jj:hh:mm:ss) Temps initial (jj:hh:mm:ss) - + Final time (jj:hh:mm:ss) Temps final (jj:hh:mm:ss) @@ -2206,122 +2379,122 @@ Seuls les 5 premiers seront édités. Pas de temps (en second) - + Minimum timestep (second) Pas de temps minimal (en second) - + Time step of writing on .TRA Pas de temps d'écriture dans le fichier .TRA - + Time step of writing on .BIN Pas de temps d'écriture dans le fichier .BIN - + Implicitation parameter - + Continuity discretization type (S/L) - + QSJ discretization (A/B) - + Stop criterion iterations (G/A/R) - + Iteration type - + Smoothing coefficient - + Maximun accepted number of CFL - + Minimum water height (meter) - + Maximun number of iterations (< 100) - + Timestep reduction factor - + Reduction precision factor of Z - + Reduction precision factor of Q - + Reduction precision factor of residue - + Number of iteration at maximum precision - + Number of iteration before switch - + Maximum accepted Froude number - + Diffluence node height balance - + Compute reach volume balance (Y/N) - + Maximum reach volume balance - + Minimum reach volume to check - + Command line arguments @@ -2331,53 +2504,53 @@ Seuls les 5 premiers seront édités. Generic - Générique + Générique Mage version 8 - Mage en version 8 + Mage en version 8 Toolbar - + Zoom Zoom - + Default view Vue par défaut - + Back to previous view Retour a la vue précédente - + Return to next view Retour à la vue suivante - + Axes panoramic Axes panoramique - + Isometric view (Shift+W) Vue isométrique (Shift+W) - + Auto scale view (Shift+X) Vue automatique (Shift+X) - + Save the figure Sauvegarder la figure diff --git a/src/motd.txt b/src/motd.txt new file mode 100644 index 00000000..bfe03bf0 --- /dev/null +++ b/src/motd.txt @@ -0,0 +1,7 @@ +`7MM"""Mq. db `7MMM. ,MMF'`7MMF' `7MMF'`YMM' `MM'`7MM"""Mq. + MM `MM. ;MM: MMMb dPMM MM MM VMA ,V MM `MM. + MM ,M9 ,V^MM. M YM ,M MM MM MM VMA ,V MM ,M9 pd*"*b. + MMmmdM9 ,M `MM M Mb M' MM MMmmmmmmMM VMMP MMmmdM9 (O) j8 + MM AbmmmqMA M YM.P' MM MM MM MM MM YM. ,;j9 + MM A' VML M `YM' MM MM MM MM MM `Mb. ,-=' +.JMML. .AMA. .AMMA..JML. `' .JMML..JMML. .JMML. .JMML. .JMML. .JMM. Ammmmmmm diff --git a/src/pamhyr.py b/src/pamhyr.py index b4bd401f..8a125fc3 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -18,7 +18,8 @@ # -*- coding: utf-8 -*- -import sys, os +import sys +import os import locale import logging @@ -34,70 +35,92 @@ from tools import ( from View.MainWindow import ApplicationWindow from Model.Study import Study +from Scripts.P3DST import Script3DST from Scripts.Hello import ScriptHello +from Scripts.ListSolver import ScriptListSolver +from Scripts.Run import ScriptExport, ScriptRun +from Scripts.MageMesh import MageMesh -from init import license, setup_lang +from init import legal_info, debug_info, setup_lang logger = logging.getLogger() scripts = { "hello": ScriptHello, + "solvers": ScriptListSolver, + "export": ScriptExport, + "run": ScriptRun, + "3DST": Script3DST, + "mesh": MageMesh, } + def usage(argv): logger.info("") logger.info(f"Usage: {argv[0]}