├── test_plugin
├── init.py
└── test_aquifer_schemata.py
├── docs
├── .gitignore
├── _static
│ ├── styles.css
│ └── QGIS-Tim-logo.svg
├── _quarto.yml
├── figures
│ ├── tutorial
│ │ ├── Left_mouse.png
│ │ ├── emoji-note.png
│ │ ├── emoji-tip.png
│ │ ├── mouse_plus.png
│ │ ├── Right_mouse.png
│ │ ├── button-PDOK.png
│ │ ├── button-pencil.png
│ │ ├── button_Mesh.png
│ │ ├── button_Raster.png
│ │ ├── emoji-warning.png
│ │ ├── figure-CPT01.png
│ │ ├── figure-PTK01.png
│ │ ├── figure-PTK02.png
│ │ ├── figure-PTK03.png
│ │ ├── figure-PTK04.png
│ │ ├── Panel-QGIS-Tim.png
│ │ ├── button-PTK-add.png
│ │ ├── button-Qgis-tim.png
│ │ ├── button_zoomPan.png
│ │ ├── icon-showlabels.png
│ │ ├── Button_CloseWindow.png
│ │ ├── Panel-QGIS-Tim_v02.png
│ │ ├── button-PDOK-search.png
│ │ ├── button-PTK-START.png
│ │ ├── button-Qgis-browse.png
│ │ ├── button_MeasureLine.png
│ │ ├── button_MoveFeature.png
│ │ ├── button_VertexTool.png
│ │ ├── button_valuetool.png
│ │ ├── button_zoom-menu.png
│ │ ├── button_zoomtolayer.png
│ │ ├── crossecties-schets.png
│ │ ├── emoji-exclamation.png
│ │ ├── figure-PTK-graph01.png
│ │ ├── figure-PTK-graph02.png
│ │ ├── figure-PTK-graph03.png
│ │ ├── figure-PTK-graph04.png
│ │ ├── figure_Well_icon.png
│ │ ├── button_OpenStreetMap.png
│ │ ├── button_Paste_Feature.png
│ │ ├── button_RotateFeature.png
│ │ ├── button_ScaleFeature.png
│ │ ├── button_SelectRaster.png
│ │ ├── button_SelectedLayer.png
│ │ ├── button_iMOD-Toolbar.png
│ │ ├── button_zoomtoextent.png
│ │ ├── crossecties-schets.pptx
│ │ ├── figure_Contourlines.png
│ │ ├── Panel-QGIS-Tim_TheHague.png
│ │ ├── Panel-Timml Aquifer L0.png
│ │ ├── button-Qgis-VectorLayer.png
│ │ ├── button_AddLineFeature.png
│ │ ├── button_AddPointFeature.png
│ │ ├── button_DeselectedLayer.png
│ │ ├── button_EnableSnapping.png
│ │ ├── button_SavelayerEdits.png
│ │ ├── button_iMOD-TimeSeries.png
│ │ ├── figure_ComputeSettings.png
│ │ ├── figure_Contourlines02.png
│ │ ├── figure_ExtractionError.png
│ │ ├── figure_HeadTimeSeries01.png
│ │ ├── figure_Observation_icon.png
│ │ ├── figure_PythonExport01.png
│ │ ├── GeoTOP-Verticaledoorsnede.pdf
│ │ ├── GeoTOP-Verticaledoorsnede.png
│ │ ├── Panel-QGIS-Tim-completion.png
│ │ ├── button_AddPolygonFeature.png
│ │ ├── button_Copy_Selected_Row.png
│ │ ├── button_iMOD-Cross-section.png
│ │ ├── figure-PTK-Calculation01.png
│ │ ├── figure-PTK-Reliability01.png
│ │ ├── figure-PTK-Sensitivity01.png
│ │ ├── figure-PTK-Sensitivity02.png
│ │ ├── figure-PTK-Sensitivity03.png
│ │ ├── figure-PTK-Uncertainty01.png
│ │ ├── figure-PTK-Uncertainty02.png
│ │ ├── figure_ValueTool-in-QGIS.png
│ │ ├── figure_WellAttributeTable.png
│ │ ├── icon-probabiliticToolKit.png
│ │ ├── photo-TheHagueCityCentre.png
│ │ ├── photo-fieldwork-liftbomb.jpg
│ │ ├── Panel-QGIS-NewShapefileLayer.png
│ │ ├── button-Qgis-AttributeTable.png
│ │ ├── button_CrossSection-ViewAll.png
│ │ ├── button_ShowSelectedFeatures.png
│ │ ├── figure-PTK-VariableNameValue.png
│ │ ├── figure_HeadsInCrosssection01.png
│ │ ├── figure_HeadsInCrosssection02.png
│ │ ├── figure_HeadsInCrosssection03.png
│ │ ├── figure_HeadsInCrosssection04.png
│ │ ├── figure_LeakageEffectCorner.png
│ │ ├── figure_Leakylinedoublet_copy.png
│ │ ├── figure_Leakylinedoublet_icon.png
│ │ ├── figure_ObservationsLocations.png
│ │ ├── figure_SheetPileExtraPoints.png
│ │ ├── figure_WellFeatureAttributes.png
│ │ ├── geohydrologie-tabel waarden.png
│ │ ├── Panel-TTim_DomainAttrTable_v01.png
│ │ ├── button-Qgis-OpenAttributeTable.png
│ │ ├── button_TemporalControler-play.png
│ │ ├── button_TemporalControllerPanel.png
│ │ ├── button_ToolbarSnappingSettings.png
│ │ ├── figure_DrawdownConstructionPit.png
│ │ ├── figure_SchematizationTheHague.png
│ │ ├── photo-TheMonumentalEnvironment.png
│ │ ├── Panel-TTim AquiferAttrTable_v01.png
│ │ ├── Panel-Timml AquiferAttrTable_v01.png
│ │ ├── Panel-Timml AquiferAttrTable_v02.png
│ │ ├── button-AttributeTable-AddFeature.png
│ │ ├── button_ToolbarSnappingTollerance.png
│ │ ├── figure_WellFeatureAttributes_v02.png
│ │ ├── button_TemporalControllerPanel-menu.png
│ │ ├── figure_DrawdownConstructionPit_v01.png
│ │ ├── Panel-TTim_ObservationsAttrTable_v01.png
│ │ ├── button-Qgis-AttributeTable-AddFeature.png
│ │ ├── button-Qgis-AttributeTable-SaveEdits.png
│ │ ├── photo-fieldwork-liftbomb-crosssection.jpg
│ │ ├── figure_LeakylinedoubletFeatureAttributes.png
│ │ └── button-Qgis-AttributeTable-StartEditingMode.png
│ └── logo
│ │ └── iMOD-tutorial.svg
├── tutorial-QGIS-TIM_files
│ └── libs
│ │ ├── bootstrap
│ │ └── bootstrap-icons.woff
│ │ └── quarto-html
│ │ ├── tippy.css
│ │ ├── quarto-syntax-highlighting.css
│ │ └── anchor.min.js
├── _extensions
│ └── quarto-ext
│ │ └── fontawesome
│ │ ├── assets
│ │ ├── webfonts
│ │ │ ├── fa-brands-400.ttf
│ │ │ ├── fa-regular-400.ttf
│ │ │ ├── fa-solid-900.ttf
│ │ │ ├── fa-solid-900.woff2
│ │ │ ├── fa-brands-400.woff2
│ │ │ ├── fa-regular-400.woff2
│ │ │ ├── fa-v4compatibility.ttf
│ │ │ └── fa-v4compatibility.woff2
│ │ └── css
│ │ │ └── latex-fontsize.css
│ │ ├── _extension.yml
│ │ └── fontawesome.lua
├── _quarto-tutorial.yml
├── tutorial.qmd
├── _quarto-website.yml
├── index.qmd
├── developer.qmd
├── deltaforge_install.qmd
└── install.qmd
├── plugin
└── qgistim
│ ├── core
│ ├── __init__.py
│ ├── elements
│ │ ├── colors.py
│ │ ├── uniform_flow.py
│ │ ├── constant.py
│ │ ├── discharge_observation.py
│ │ ├── leaky_line_doublet.py
│ │ ├── polygon_area_sink.py
│ │ ├── impermeable_line_doublet.py
│ │ ├── polygon_semi_confined_top.py
│ │ ├── observation.py
│ │ ├── circular_area_sink.py
│ │ ├── head_line_sink.py
│ │ ├── line_sink_ditch.py
│ │ ├── polygon_inhomogeneity.py
│ │ ├── domain.py
│ │ ├── building_pit.py
│ │ ├── __init__.py
│ │ ├── well.py
│ │ ├── leaky_building_pit.py
│ │ ├── headwell.py
│ │ ├── schemata.py
│ │ └── aquifer.py
│ ├── task.py
│ ├── install_backend.py
│ ├── geopackage.py
│ ├── extractor.py
│ ├── server_handler.py
│ └── layer_styling.py
│ ├── widgets
│ ├── __init__.py
│ ├── error_window.py
│ ├── elements_widget.py
│ └── install_dialog.py
│ ├── icon.png
│ ├── resources.qrc
│ ├── test
│ └── __init__.py
│ ├── __init__.py
│ ├── README.txt
│ ├── metadata.txt
│ ├── qgistim.py
│ └── qt
│ ├── qgistim_name_dialog_base.ui
│ ├── qgistim_conda_env.ui
│ └── qgistim_radius_dialog_base.ui
├── gistim
├── __init__.py
├── test
│ └── test_elements.py
├── geomet
│ ├── __init__.py
│ └── util.py
├── netcdf.py
├── __main__.py
└── geopackage.py
├── .gitattributes
├── .github
├── workflows
│ ├── create_plugin_zip.py
│ ├── docs.yml
│ ├── create_pyinstaller_zip.py
│ └── release.yml
└── dependabot.yml
├── scripts
├── deploy.bat
├── package.bat
└── run_tests.bat
├── .pre-commit-config.yaml
├── tox.ini
├── pixi.toml
├── .gitignore
├── pyproject.toml
└── README.md
/test_plugin/init.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /.quarto/
2 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/plugin/qgistim/widgets/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/_static/styles.css:
--------------------------------------------------------------------------------
1 | /* css styles */
--------------------------------------------------------------------------------
/gistim/__init__.py:
--------------------------------------------------------------------------------
1 | import gistim.compute
2 |
3 | __version__ = "0.6.0"
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # GitHub syntax highlighting
2 | pixi.lock linguist-language=YAML
3 |
4 |
--------------------------------------------------------------------------------
/plugin/qgistim/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/plugin/qgistim/icon.png
--------------------------------------------------------------------------------
/gistim/test/test_elements.py:
--------------------------------------------------------------------------------
1 | import gistim
2 |
3 |
4 | def test_import():
5 | assert gistim is not None
6 |
--------------------------------------------------------------------------------
/docs/_quarto.yml:
--------------------------------------------------------------------------------
1 | project:
2 | output-dir: _build
3 |
4 | profile:
5 | group:
6 | - [website, tutorial]
7 |
--------------------------------------------------------------------------------
/docs/figures/tutorial/Left_mouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Left_mouse.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/emoji-note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/emoji-note.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/emoji-tip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/emoji-tip.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/mouse_plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/mouse_plus.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Right_mouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Right_mouse.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-PDOK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-PDOK.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-pencil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-pencil.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_Mesh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_Mesh.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_Raster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_Raster.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/emoji-warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/emoji-warning.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-CPT01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-CPT01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK03.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK04.png
--------------------------------------------------------------------------------
/.github/workflows/create_plugin_zip.py:
--------------------------------------------------------------------------------
1 | import shutil
2 |
3 | zippath = shutil.make_archive(f"./dist/QGIS-Tim-plugin", "zip", "./plugin")
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-QGIS-Tim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-QGIS-Tim.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-PTK-add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-PTK-add.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-tim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-tim.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_zoomPan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_zoomPan.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/icon-showlabels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/icon-showlabels.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Button_CloseWindow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Button_CloseWindow.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-QGIS-Tim_v02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-QGIS-Tim_v02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-PDOK-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-PDOK-search.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-PTK-START.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-PTK-START.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-browse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-browse.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_MeasureLine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_MeasureLine.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_MoveFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_MoveFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_VertexTool.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_VertexTool.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_valuetool.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_valuetool.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_zoom-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_zoom-menu.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_zoomtolayer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_zoomtolayer.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/crossecties-schets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/crossecties-schets.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/emoji-exclamation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/emoji-exclamation.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-graph01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-graph01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-graph02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-graph02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-graph03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-graph03.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-graph04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-graph04.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_Well_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_Well_icon.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_OpenStreetMap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_OpenStreetMap.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_Paste_Feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_Paste_Feature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_RotateFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_RotateFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_ScaleFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_ScaleFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_SelectRaster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_SelectRaster.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_SelectedLayer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_SelectedLayer.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_iMOD-Toolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_iMOD-Toolbar.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_zoomtoextent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_zoomtoextent.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/crossecties-schets.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/crossecties-schets.pptx
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_Contourlines.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_Contourlines.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-QGIS-Tim_TheHague.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-QGIS-Tim_TheHague.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-Timml Aquifer L0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-Timml Aquifer L0.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-VectorLayer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-VectorLayer.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_AddLineFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_AddLineFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_AddPointFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_AddPointFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_DeselectedLayer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_DeselectedLayer.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_EnableSnapping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_EnableSnapping.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_SavelayerEdits.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_SavelayerEdits.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_iMOD-TimeSeries.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_iMOD-TimeSeries.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_ComputeSettings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_ComputeSettings.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_Contourlines02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_Contourlines02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_ExtractionError.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_ExtractionError.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_HeadTimeSeries01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_HeadTimeSeries01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_Observation_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_Observation_icon.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_PythonExport01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_PythonExport01.png
--------------------------------------------------------------------------------
/plugin/qgistim/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | icon.png
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/figures/tutorial/GeoTOP-Verticaledoorsnede.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/GeoTOP-Verticaledoorsnede.pdf
--------------------------------------------------------------------------------
/docs/figures/tutorial/GeoTOP-Verticaledoorsnede.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/GeoTOP-Verticaledoorsnede.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-QGIS-Tim-completion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-QGIS-Tim-completion.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_AddPolygonFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_AddPolygonFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_Copy_Selected_Row.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_Copy_Selected_Row.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_iMOD-Cross-section.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_iMOD-Cross-section.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-Calculation01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-Calculation01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-Reliability01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-Reliability01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-Sensitivity01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-Sensitivity01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-Sensitivity02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-Sensitivity02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-Sensitivity03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-Sensitivity03.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-Uncertainty01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-Uncertainty01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-Uncertainty02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-Uncertainty02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_ValueTool-in-QGIS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_ValueTool-in-QGIS.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_WellAttributeTable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_WellAttributeTable.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/icon-probabiliticToolKit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/icon-probabiliticToolKit.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/photo-TheHagueCityCentre.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/photo-TheHagueCityCentre.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/photo-fieldwork-liftbomb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/photo-fieldwork-liftbomb.jpg
--------------------------------------------------------------------------------
/plugin/qgistim/test/__init__.py:
--------------------------------------------------------------------------------
1 | # TODO
2 | # import qgis libs so that ve set the correct sip api version
3 | import qgis # pylint: disable=W0611 # NOQA
4 |
--------------------------------------------------------------------------------
/scripts/deploy.bat:
--------------------------------------------------------------------------------
1 | set plugin_dir=%APPDATA%\QGIS\QGIS3\profiles\default\python\plugins
2 |
3 | robocopy %~dp0\..\plugin\qgistim %plugin_dir%\qgistim /E
4 |
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-QGIS-NewShapefileLayer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-QGIS-NewShapefileLayer.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-AttributeTable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-AttributeTable.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_CrossSection-ViewAll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_CrossSection-ViewAll.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_ShowSelectedFeatures.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_ShowSelectedFeatures.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure-PTK-VariableNameValue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure-PTK-VariableNameValue.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_HeadsInCrosssection01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_HeadsInCrosssection01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_HeadsInCrosssection02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_HeadsInCrosssection02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_HeadsInCrosssection03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_HeadsInCrosssection03.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_HeadsInCrosssection04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_HeadsInCrosssection04.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_LeakageEffectCorner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_LeakageEffectCorner.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_Leakylinedoublet_copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_Leakylinedoublet_copy.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_Leakylinedoublet_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_Leakylinedoublet_icon.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_ObservationsLocations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_ObservationsLocations.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_SheetPileExtraPoints.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_SheetPileExtraPoints.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_WellFeatureAttributes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_WellFeatureAttributes.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/geohydrologie-tabel waarden.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/geohydrologie-tabel waarden.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-TTim_DomainAttrTable_v01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-TTim_DomainAttrTable_v01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-OpenAttributeTable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-OpenAttributeTable.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_TemporalControler-play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_TemporalControler-play.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_TemporalControllerPanel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_TemporalControllerPanel.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_ToolbarSnappingSettings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_ToolbarSnappingSettings.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_DrawdownConstructionPit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_DrawdownConstructionPit.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_SchematizationTheHague.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_SchematizationTheHague.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/photo-TheMonumentalEnvironment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/photo-TheMonumentalEnvironment.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-TTim AquiferAttrTable_v01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-TTim AquiferAttrTable_v01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-Timml AquiferAttrTable_v01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-Timml AquiferAttrTable_v01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-Timml AquiferAttrTable_v02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-Timml AquiferAttrTable_v02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-AttributeTable-AddFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-AttributeTable-AddFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_ToolbarSnappingTollerance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_ToolbarSnappingTollerance.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_WellFeatureAttributes_v02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_WellFeatureAttributes_v02.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button_TemporalControllerPanel-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button_TemporalControllerPanel-menu.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_DrawdownConstructionPit_v01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_DrawdownConstructionPit_v01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/Panel-TTim_ObservationsAttrTable_v01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/Panel-TTim_ObservationsAttrTable_v01.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-AttributeTable-AddFeature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-AttributeTable-AddFeature.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-AttributeTable-SaveEdits.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-AttributeTable-SaveEdits.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/photo-fieldwork-liftbomb-crosssection.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/photo-fieldwork-liftbomb-crosssection.jpg
--------------------------------------------------------------------------------
/docs/tutorial-QGIS-TIM_files/libs/bootstrap/bootstrap-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/tutorial-QGIS-TIM_files/libs/bootstrap/bootstrap-icons.woff
--------------------------------------------------------------------------------
/docs/figures/tutorial/figure_LeakylinedoubletFeatureAttributes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/figure_LeakylinedoubletFeatureAttributes.png
--------------------------------------------------------------------------------
/docs/figures/tutorial/button-Qgis-AttributeTable-StartEditingMode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/figures/tutorial/button-Qgis-AttributeTable-StartEditingMode.png
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.ttf
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/QGIS-Tim/master/docs/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.woff2
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/_extension.yml:
--------------------------------------------------------------------------------
1 | title: Font Awesome support
2 | author: Carlos Scheidegger
3 | version: 1.1.0
4 | quarto-required: ">=1.2.269"
5 | contributes:
6 | shortcodes:
7 | - fontawesome.lua
8 |
--------------------------------------------------------------------------------
/plugin/qgistim/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This script initializes the plugin, making it known to QGIS.
3 | """
4 |
5 |
6 | def classFactory(iface): # pylint: disable=invalid-name
7 | from .qgistim import QgisTimPlugin
8 |
9 | return QgisTimPlugin(iface)
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2 | version: 2
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/" # Location of package manifests
6 | schedule:
7 | interval: "weekly"
--------------------------------------------------------------------------------
/scripts/package.bat:
--------------------------------------------------------------------------------
1 |
2 | rem powershell equivalent to https://github.com/lutraconsulting/qgis-crayfish-plugin/blob/master/package.bash
3 |
4 | %systemroot%\System32\WindowsPowerShell\v1.0\powershell.exe -command "cd ..; rm -r -fo plugin/qgistim.zip; cd plugin/qgistim; git archive --prefix=qgistim/ -o ../qgistim.zip HEAD"
5 |
6 | pause
--------------------------------------------------------------------------------
/docs/_quarto-tutorial.yml:
--------------------------------------------------------------------------------
1 | project:
2 | type: book
3 |
4 | book:
5 | title: "QGIS Tim Tutorial"
6 | chapters:
7 | - index.qmd
8 | - tutorial.qmd
9 | - tutorial_Rijsenhout.qmd
10 | - tutorial_TheHague.qmd
11 |
12 | format:
13 | pdf:
14 | documentclass: scrreprt
15 | top-level-division: chapter
16 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/PyCQA/isort
3 | rev: 5.12.0
4 | hooks:
5 | - id: isort
6 |
7 | - repo: https://github.com/psf/black
8 | rev: 23.1.0
9 | hooks:
10 | - id: black
11 |
12 | - repo: https://github.com/PyCQA/flake8
13 | rev: 6.0.0
14 | hooks:
15 | - id: flake8
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/colors.py:
--------------------------------------------------------------------------------
1 | RED = "215,48,39,255"
2 | GREEN = "51,160,44,255"
3 | BLUE = "31,120,180,255"
4 | GREY = "135,135,135,255"
5 | BLACK = "0,0,0,255"
6 | LIGHT_BLUE = "166,206,227,255"
7 |
8 | OPACITY = 51 # 20%
9 | TRANSPARENT = "255,255,255,0"
10 | TRANSPARENT_RED = f"215,48,39,{OPACITY}"
11 | TRANSPARENT_GREEN = f"51,160,44,{OPACITY}"
12 | TRANSPARENT_BLUE = f"31,120,180,{OPACITY}"
13 | TRANSPARENT_GREY = f"135,135,135,{OPACITY}"
14 |
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/assets/css/latex-fontsize.css:
--------------------------------------------------------------------------------
1 | .fa-tiny {
2 | font-size: 0.5em;
3 | }
4 | .fa-scriptsize {
5 | font-size: 0.7em;
6 | }
7 | .fa-footnotesize {
8 | font-size: 0.8em;
9 | }
10 | .fa-small {
11 | font-size: 0.9em;
12 | }
13 | .fa-normalsize {
14 | font-size: 1em;
15 | }
16 | .fa-large {
17 | font-size: 1.2em;
18 | }
19 | .fa-Large {
20 | font-size: 1.5em;
21 | }
22 | .fa-LARGE {
23 | font-size: 1.75em;
24 | }
25 | .fa-huge {
26 | font-size: 2em;
27 | }
28 | .fa-Huge {
29 | font-size: 2.5em;
30 | }
31 |
--------------------------------------------------------------------------------
/scripts/run_tests.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | call "c:\Program Files\QGIS 3.30.1\bin\o4w_env.bat"
3 | path %OSGEO4W_ROOT%\apps\qgis\bin;%PATH%
4 | set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/qgis
5 | set GDAL_FILENAME_IS_UTF8=YES
6 | rem Set VSI cache to be used as buffer, see #6448
7 | set VSI_CACHE=TRUE
8 | set VSI_CACHE_SIZE=1000000
9 | set QT_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\qgis\qtplugins;%OSGEO4W_ROOT%\apps\qt5\plugins
10 | set PYTHONPATH=%OSGEO4W_ROOT%\apps\qgis\python;%PYTHONPATH%
11 | set PYTHONPATH=%CD%\..\plugin;%PYTHONPATH%
12 | @echo on
13 | python -m unittest discover --start-directory ..\test_plugin -v
14 | PAUSE
15 |
--------------------------------------------------------------------------------
/docs/tutorial.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "{{< fa solid book >}} Tutorials"
3 | listing:
4 | type: grid
5 | contents:
6 | - tutorial_Rijsenhout.qmd
7 | - tutorial_TheHague.qmd
8 | image: figures/logo/iMOD-tutorial.svg
9 | description: 'Learn how to use QGIS Tim plugin'
10 | index: "5"
11 | ---
12 | Modelling with analytic elements is not very common. The QGIS-Tim plugin makes it more easy to build your model in a graphical user interface starting from simple and making it as complex as you like.
13 |
14 | Therefore, we have compiled a set of tutorials to help you get started and give you an overview of the capabilities of analytical elements.
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/plugin/qgistim/README.txt:
--------------------------------------------------------------------------------
1 | This QGIS plugin serves to easily create model input TimML and TTim analytic
2 | element models:
3 |
4 | * https://github.com/mbakker7/timml
5 | * https://github.com/mbakker7/ttim
6 |
7 | Analytic element input is typically a collection of points, lines, and polygons.
8 | This plugin represent a single model with a single GeoPackage. The different
9 | elements are represented as layers in the GeoPackage, and also as ordinary
10 | vector layers in QGIS.
11 |
12 | To run the model with TimML and TTim, this plugin relies on a server program called
13 | `gistim`.
14 |
15 | Find the repository and issue tracker here:
16 | https://github.com/Deltares/QGIS-Tim
17 |
--------------------------------------------------------------------------------
/gistim/geomet/__init__.py:
--------------------------------------------------------------------------------
1 | # These modules are copied from: https://github.com/geomet/geomet
2 | # Which has the following copyright notice:
3 | #
4 | #
5 | # Copyright 2013 Lars Butler & individual contributors
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | basepython = 3.9
3 | isolated_build = True
4 |
5 | [testenv:test]
6 | commands =
7 | pytest gistim/test/
8 | deps =
9 | pytest
10 |
11 | [testenv:docs]
12 | commands = sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" --color -W -bhtml
13 | python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))'
14 | deps =
15 | sphinx
16 | pydata_sphinx_theme
17 |
18 | [testenv:format]
19 | skip_install = True
20 | commands =
21 | isort .
22 | black .
23 | deps =
24 | black
25 | isort
26 |
27 | [testenv:lint]
28 | skip_install = True
29 | commands =
30 | isort --check .
31 | black --check .
32 | flake8 .
33 | deps =
34 | black
35 | flake8
36 | isort
37 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | # Based on: https://tarleb.com/posts/quarto-with-gh-pages/
2 | name: Publish Website
3 |
4 | # Allow one concurrent deployment
5 | concurrency:
6 | group: "pages"
7 | cancel-in-progress: true
8 |
9 | on:
10 | push:
11 | branches: ['master']
12 |
13 | jobs:
14 | quarto-publish:
15 | name: Publish with Quarto
16 | runs-on: ubuntu-latest
17 | steps:
18 | # Circumvent this problem: https://github.com/actions/checkout/issues/165
19 | - name: Checkout code with LFS cache
20 | uses: nschloe/action-checkout-with-lfs-cache@v1
21 |
22 | - name: Install Quarto
23 | uses: quarto-dev/quarto-actions/setup@v2
24 |
25 | - name: Render HTML
26 | run: "quarto render docs --profile website --to html"
27 |
28 | - name: Deploy HTML to github pages
29 | uses: peaceiris/actions-gh-pages@v4
30 | with:
31 | github_token: ${{ secrets.GITHUB_TOKEN }}
32 | publish_dir: docs/_build
33 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/uniform_flow.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsField
3 | from qgistim.core.elements.element import Element
4 | from qgistim.core.elements.schemata import SingleRowSchema
5 | from qgistim.core.schemata import Required, RequiresConfinedAquifer
6 |
7 |
8 | class UniformFlowSchema(SingleRowSchema):
9 | timml_schemata = {
10 | "slope": Required(),
11 | "angle": Required(),
12 | }
13 | timml_consistency_schemata = (RequiresConfinedAquifer(),)
14 |
15 |
16 | class UniformFlow(Element):
17 | element_type = "Uniform Flow"
18 | geometry_type = "No geometry"
19 | timml_attributes = (
20 | QgsField("slope", QVariant.Double),
21 | QgsField("angle", QVariant.Double),
22 | QgsField("label", QVariant.String),
23 | )
24 | schema = UniformFlowSchema()
25 |
26 | def process_timml_row(self, row, other=None):
27 | return {
28 | "slope": row["slope"],
29 | "angle": row["angle"],
30 | "label": row["label"],
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/create_pyinstaller_zip.py:
--------------------------------------------------------------------------------
1 | import json
2 | import hashlib
3 | import os
4 | import platform
5 | import shutil
6 |
7 |
8 | def write_versions(path):
9 | import timml
10 | import gistim
11 | import ttim
12 |
13 | versions = {
14 | "timml": timml.__version__,
15 | "ttim": ttim.__version__,
16 | "gistim": gistim.__version__,
17 | }
18 | with open(path, "w") as f:
19 | json.dump(versions, f)
20 | return
21 |
22 |
23 | # Write versions into PyInstaller directory
24 | write_versions("./dist/gistim/versions.json")
25 |
26 | # Create archive
27 | # Use the RUNNER_OS variable on the Github Runner. Use platform system locally.
28 | system = os.environ.get("RUNNER_OS", platform.system())
29 | zippath = shutil.make_archive(f"./dist/gistim-{system}", "zip", "./dist/gistim")
30 |
31 | # Create a checksum
32 | with open(zippath, "rb", buffering=0) as f:
33 | sha256_hash = hashlib.file_digest(f, "sha256").hexdigest()
34 |
35 | txt_path = f"./dist/sha256-checksum-{system}.txt"
36 | with open(txt_path, "w") as f:
37 | f.write(sha256_hash)
38 |
--------------------------------------------------------------------------------
/docs/_quarto-website.yml:
--------------------------------------------------------------------------------
1 | project:
2 | type: website
3 |
4 | website:
5 | title: "QGIS-Tim"
6 | page-navigation: true
7 | navbar:
8 | background: "#080c80"
9 | logo: _static/iMOD-doc-logo.svg
10 | left:
11 | - text: Installation
12 | href: install.qmd
13 | - text: Tutorials
14 | href: tutorial.qmd
15 | - text: Development
16 | href: developer.qmd
17 | right:
18 | - icon: github
19 | href: https://github.com/Deltares/QGIS-Tim
20 | aria-label: GitHub
21 |
22 | sidebar:
23 | - id: Installation
24 | title: "Installation"
25 | style: "floating"
26 | collapse-level: 2
27 | align: left
28 |
29 | - id: tutorial
30 | title: "Tutorials"
31 | style: "floating"
32 | collapse-level: 2
33 | align: left
34 | contents:
35 | - href: tutorial.qmd
36 | - href: tutorial_Rijsenhout.qmd
37 | - href: tutorial_TheHague.qmd
38 |
39 | format:
40 | html:
41 | theme: cosmo
42 | css: _static/styles.css
43 | toc: true
44 | code-fold: true
45 |
46 |
--------------------------------------------------------------------------------
/docs/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "QGIS-Tim"
3 | listing:
4 | type: grid
5 | image-height: 250px
6 | contents:
7 | - tutorial.qmd
8 | sort: "index"
9 | ---
10 |
11 | QGIS-Tim is an open source project for multi-layer groundwater flow
12 | simulations. QGIS-Tim provides a link between QGIS and the open source analytic
13 | element method software: [TimML (steady-state)](https://github.com/mbakker7/timml)
14 | and [TTim (transient)](https://github.com/mbakker7/ttim).
15 |
16 | The benefit of the analytic element method (AEM) is that no grid or
17 | time-stepping is required. Geohydrological features are represented by points,
18 | lines, and polygons. QGIS-Tim stores these features in a
19 | [GeoPackage](https://www.geopackage.org/).
20 |
21 | QGIS-Tim consists of a "front-end" and a "back-end". The front-end is a QGIS
22 | plugin that provides a limited graphical interface to setup model input,
23 | visualize, and analyze model input. The back-end is a Python package. It reads
24 | the contents of the GeoPackage and transforms it into a TimML or TTim model,
25 | computes a result, and writes it to a file that the QGIS plugin loads.
26 |
--------------------------------------------------------------------------------
/pixi.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | name = "gistim"
3 | version = "0.6.0"
4 | description = "Connects TimML and TTim Analytic Element modeling to QGIS"
5 | authors = ["Huite Bootsma"]
6 | channels = ["conda-forge"]
7 | platforms = ["win-64", "linux-64", "osx-64"]
8 | license-file = "LICENSE"
9 | homepage = "https://deltares.github.io/QGIS-Tim"
10 | documentation = "https://deltares.github.io/QGIS-Tim"
11 | repository = "https://github.com/Deltares/qgis-tim"
12 |
13 | [tasks]
14 | install = "pip install --no-deps --editable ."
15 | build-backend = "pyinstaller gistim/__main__.py --name gistim"
16 | zip-backend = {depends-on = "install", cmd = "python .github/workflows/create_pyinstaller_zip.py"}
17 | zip-plugin = "python .github/workflows/create_plugin_zip.py"
18 |
19 | [dependencies]
20 | python = "3.11.*"
21 | future = "*"
22 | pip = "*"
23 |
24 | # Conda-forge will distribute numpy and scipy with Intel MKL, which adds 600 MB
25 | # to the pyinstaller result. By installing from PyPI instead, we can skip MKL.
26 | # We do need to declare `future` explicitly, as pixi will complain that on PyPI,
27 | # no wheels can be found.
28 | [pypi-dependencies]
29 | lmfit = "*"
30 | numpy = "*"
31 | pandas = "*"
32 | pyinstaller = "*"
33 | timml = "==6.3.0"
34 | ttim = "==0.6.5"
35 | scipy = "*"
36 | xarray = "*"
37 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - '*'
9 |
10 | jobs:
11 | build:
12 | name: Release ${{ matrix.os }}
13 | runs-on: ${{ matrix.os }}
14 | permissions: write-all
15 | strategy:
16 | matrix:
17 | os:
18 | - ubuntu-latest
19 | - macOS-latest
20 | - windows-latest
21 | steps:
22 | - name: Check out repo
23 | uses: actions/checkout@v6
24 | - name: Setup Pixi
25 | uses: prefix-dev/setup-pixi@v0.9.3
26 | - name: Build with PyInstaller
27 | run: pixi run build-backend
28 | - name: Create backend ZIP
29 | run: pixi run zip-backend
30 | - name: Release backend ZIP
31 | run: gh release upload ${{ github.ref_name }} dist/gistim-${{ runner.os }}.zip dist/sha256-checksum-${{ runner.os }}.txt
32 | env:
33 | GITHUB_TOKEN: ${{ github.TOKEN }}
34 | - name: Create plugin ZIP
35 | if: matrix.os == 'ubuntu-latest'
36 | run: pixi run zip-plugin
37 | - name: Release plugin ZIP
38 | if: matrix.os == 'ubuntu-latest'
39 | run: gh release upload ${{ github.ref_name }} dist/QGIS-Tim-plugin.zip
40 | env:
41 | GITHUB_TOKEN: ${{ github.TOKEN }}
42 |
43 |
--------------------------------------------------------------------------------
/docs/tutorial-QGIS-TIM_files/libs/quarto-html/tippy.css:
--------------------------------------------------------------------------------
1 | .tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/constant.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsField, QgsSingleSymbolRenderer
3 | from qgistim.core.elements.colors import RED
4 | from qgistim.core.elements.element import Element
5 | from qgistim.core.elements.schemata import SingleRowSchema
6 | from qgistim.core.schemata import Membership, Required, RequiresConfinedAquifer
7 |
8 |
9 | class ConstantSchema(SingleRowSchema):
10 | timml_schemata = {
11 | "geometry": Required(),
12 | "head": Required(),
13 | "layer": Required(Membership("aquifer layers")),
14 | }
15 | timml_consistency_schemata = (RequiresConfinedAquifer(),)
16 |
17 |
18 | class Constant(Element):
19 | element_type = "Constant"
20 | geometry_type = "Point"
21 | timml_attributes = (
22 | QgsField("head", QVariant.Double),
23 | QgsField("layer", QVariant.Int),
24 | QgsField("label", QVariant.String),
25 | )
26 | schema = ConstantSchema()
27 |
28 | @classmethod
29 | def renderer(cls) -> QgsSingleSymbolRenderer:
30 | return cls.marker_renderer(color=RED, name="star", size="5")
31 |
32 | def process_timml_row(self, row, other=None):
33 | x, y = self.point_xy(row)
34 | return {
35 | "xr": x,
36 | "yr": y,
37 | "hr": row["head"],
38 | "layer": row["layer"],
39 | "label": row["label"],
40 | }
41 |
--------------------------------------------------------------------------------
/plugin/qgistim/metadata.txt:
--------------------------------------------------------------------------------
1 | # This file contains metadata for your plugin.
2 |
3 | # This file should be included when you package your plugin.# Mandatory items:
4 |
5 | [general]
6 | name=Qgis-Tim
7 | qgisMinimumVersion=3.28
8 | description=QGIS plugin to setup TimML multi-layer analytic elements
9 | version=0.6.0
10 | author=Deltares
11 | email=huitebootsma@gmail.com
12 |
13 | about=QGIS plugin to setup TimML multi-layer analytic elements
14 |
15 | tracker=https://github.com/Deltares/QGIS-Tim/issues
16 | repository=https://github.com/Deltares/QGIS-Tim
17 | # End of mandatory metadata
18 |
19 | # Recommended items:
20 |
21 | hasProcessingProvider=no
22 | # Uncomment the following line and add your changelog:
23 | # changelog=
24 |
25 | # Tags are comma separated with spaces allowed
26 | tags=python, groundwater, groundwater modeling, analytic element, TTim, TimML
27 |
28 | homepage=https://deltares.github.io/QGIS-Tim/
29 | category=Plugins
30 | icon=icon.png
31 | # experimental flag
32 | experimental=False
33 |
34 | # deprecated flag (applies to the whole plugin, not just a single version)
35 | deprecated=False
36 |
37 | # Since QGIS 3.8, a comma separated list of plugins to be installed
38 | # (or upgraded) can be specified.
39 | # Check the documentation for more information.
40 | # plugin_dependencies=
41 |
42 | Category of the plugin: Raster, Vector, Database or Web
43 | # category=
44 |
45 | # If the plugin can run on QGIS Server.
46 | server=False
47 |
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # file based on github/gitignore
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | .pytest_cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | .hypothesis/
51 |
52 | # Sphinx documentation
53 | docs/_build/
54 |
55 | # PyBuilder
56 | target/
57 |
58 | # Jupyter Notebook
59 | .ipynb_checkpoints
60 |
61 | # pyenv
62 | .python-version
63 |
64 | # Environments
65 | .env
66 | .venv
67 | env/
68 | venv/
69 | ENV/
70 | env.bak/
71 | venv.bak/
72 |
73 | # Spyder project settings
74 | .spyderproject
75 | .spyproject
76 |
77 | # VScode
78 | .vscode
79 |
80 | # PyCharm
81 | .idea
82 |
83 | # sphinx gallery output
84 | docs/auto_examples
85 | gistim/examples/data/
86 |
87 | plugin/qgistim.zip
88 | # pixi environments
89 | .pixi/
90 |
91 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=64.0.0", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "gistim"
7 | description = "Connects TimML and TTim Analytic Element modeling to QGIS"
8 | readme = "README.md"
9 | version = "0.6.0"
10 | maintainers = [{ name = "Huite Bootsma", email = "huite.bootsma@deltares.nl" }]
11 | requires-python = ">=3.9"
12 | dependencies = [
13 | 'pandas',
14 | 'timml>=6.3.0',
15 | 'ttim>=0.6.5',
16 | 'xarray',
17 | ]
18 | classifiers = [
19 | 'Development Status :: 4 - Beta',
20 | 'Intended Audience :: Science/Research',
21 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
22 | 'Programming Language :: Python',
23 | 'Operating System :: OS Independent',
24 | 'Programming Language :: Python :: 3.9',
25 | 'Programming Language :: Python :: 3.10',
26 | 'Programming Language :: Python :: 3.11',
27 | 'Programming Language :: Python :: Implementation :: CPython',
28 | 'Topic :: Scientific/Engineering',
29 | ]
30 | keywords = ['groundwater modeling', 'analytic element']
31 | license = { text = "GPL-2.0" }
32 |
33 | [project.urls]
34 | Home = "https://github.com/deltares/QGIS-Tim"
35 | Code = "https://github.com/deltares/QGIS-Tim"
36 | Issues = "https://github.com/deltares/QGIS-Tim/issues"
37 |
38 | [tool.setuptools]
39 | packages = [
40 | "gistim",
41 | "gistim.geomet",
42 | ]
43 | license-files = ["LICENSE"]
44 |
45 | [tool.isort]
46 | profile = "black"
47 |
--------------------------------------------------------------------------------
/gistim/netcdf.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | from typing import Union
3 |
4 | import numpy as np
5 | import xarray as xr
6 |
7 | from gistim.geopackage import CoordinateReferenceSystem
8 | from gistim.ugrid import to_ugrid2d
9 |
10 |
11 | def write_raster(
12 | head: xr.DataArray,
13 | crs: CoordinateReferenceSystem,
14 | outpath: Union[pathlib.Path, str],
15 | ) -> None:
16 | # Write GDAL required metadata.
17 | head["spatial_ref"] = xr.DataArray(
18 | np.int32(0), attrs={"crs_wkt": crs.wkt, "spatial_ref": crs.wkt}
19 | )
20 | head.attrs["grid_mapping"] = "spatial_ref"
21 | head["x"].attrs = {
22 | "axis": "X",
23 | "long_name": "x coordinate of projection",
24 | "standard_name": "projection_x_coordinate",
25 | }
26 | head["y"].attrs = {
27 | "axis": "Y",
28 | "long_name": "y coordinate of projection",
29 | "standard_name": "projection_y_coordinate",
30 | }
31 | head.to_netcdf(outpath.with_suffix(".nc"), format="NETCDF3_CLASSIC")
32 | return
33 |
34 |
35 | def write_ugrid(
36 | head: xr.DataArray,
37 | crs: CoordinateReferenceSystem,
38 | outpath: Union[pathlib.Path, str],
39 | ) -> None:
40 | ugrid_head = to_ugrid2d(head)
41 | # Write MDAL required metadata.
42 | ugrid_head["projected_coordinate_system"] = xr.DataArray(
43 | data=np.int32(0),
44 | attrs={"epsg": np.int32(crs.srs_id)},
45 | )
46 | ugrid_head.to_netcdf(outpath.with_suffix(".ugrid.nc"), format="NETCDF3_CLASSIC")
47 | return
48 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/discharge_observation.py:
--------------------------------------------------------------------------------
1 |
2 | from PyQt5.QtCore import QVariant
3 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
4 | from qgistim.core.elements.colors import LIGHT_BLUE
5 | from qgistim.core.elements.element import Element
6 | from qgistim.core.elements.schemata import RowWiseSchema
7 | from qgistim.core.schemata import (
8 | Positive,
9 | Required,
10 | )
11 |
12 |
13 | class DischargeObservationSchema(RowWiseSchema):
14 | timml_schemata = {
15 | "geometry": Required(),
16 | "legendre_method": Required(),
17 | "ndegrees": Required(Positive()),
18 | }
19 |
20 |
21 | class DischargeObservation(Element):
22 | element_type = "Discharge Observation"
23 | geometry_type = "Linestring"
24 | timml_attributes = (
25 | QgsField("legendre_method", QVariant.Bool),
26 | QgsField("ndegrees", QVariant.Int),
27 | QgsField("label", QVariant.String)
28 | )
29 | timml_defaults = {
30 | "legendre_method": QgsDefaultValue("True"),
31 | "ndegrees": QgsDefaultValue("10"),
32 | }
33 | schema = DischargeObservationSchema()
34 |
35 | @classmethod
36 | def renderer(cls) -> QgsSingleSymbolRenderer:
37 | return cls.line_renderer(color=LIGHT_BLUE, width="0.75", outline_style="dash")
38 |
39 | def process_timml_row(self, row, other=None):
40 | return {
41 | "xy": self.linestring_xy(row),
42 | "method": "legendre" if row["legendre_method"] else "quad",
43 | "ndeg": row["ndegrees"],
44 | "label": row["label"],
45 | }
46 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/leaky_line_doublet.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
3 | from qgistim.core.elements.colors import RED
4 | from qgistim.core.elements.element import Element
5 | from qgistim.core.elements.schemata import RowWiseSchema
6 | from qgistim.core.schemata import Membership, Positive, Required, StrictlyPositive
7 |
8 |
9 | class LeakyLineDoubletSchema(RowWiseSchema):
10 | timml_schemata = {
11 | "geometry": Required(),
12 | "resistance": Required(StrictlyPositive()),
13 | "order": Required(Positive()),
14 | "layer": Required(Membership("aquifer layers")),
15 | }
16 |
17 |
18 | class LeakyLineDoublet(Element):
19 | element_type = "Leaky Line Doublet"
20 | geometry_type = "Linestring"
21 | timml_attributes = (
22 | QgsField("resistance", QVariant.Double),
23 | QgsField("order", QVariant.Int),
24 | QgsField("layer", QVariant.Int),
25 | QgsField("label", QVariant.String),
26 | )
27 | timml_defaults = {
28 | "order": QgsDefaultValue("4"),
29 | }
30 | schema = LeakyLineDoubletSchema()
31 |
32 | @classmethod
33 | def renderer(cls) -> QgsSingleSymbolRenderer:
34 | return cls.line_renderer(color=RED, width="0.75", outline_style="dash")
35 |
36 | def process_timml_row(self, row, other=None):
37 | return {
38 | "xy": self.linestring_xy(row),
39 | "res": row["resistance"],
40 | "layers": row["layer"],
41 | "order": row["order"],
42 | "label": row["label"],
43 | }
44 |
--------------------------------------------------------------------------------
/docs/developer.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Developer Notes"
3 | ---
4 |
5 | ## Pixi
6 | QGIS-Tim uses pixi to manage installing dependencies and run common tasks.
7 |
8 | Follow the instructions on the [Getting Started page](https://pixi.sh/).
9 |
10 | ### PyInstaller
11 | To build the TimML and TTim server application with PyInstaller, run `pixi run build-backend`
12 |
13 | This creates a built PyInstaller application in `./dist/gistim`.
14 |
15 | Run `pixi run zip-backend` to create a ZIP file of the PyInstaller application.
16 | Run `pixi run zip-plugin` to create a ZIP file of the QGIS plugin that can be installed in QGIS.
17 |
18 | To test the created ZIP files: Install the QGIS from the ZIP file, start the QGIS plugin and try to install the ZIP file via the "Install TimML and TTim server" button.
19 |
20 | ## Creating new release
21 |
22 | ### Make a GitHub release
23 |
24 | To create a new release:
25 |
26 | 1. Go to the [QGIS-Tim releases page](https://github.com/Deltares/QGIS-Tim/releases)
27 | 2. Click on the "Draft a new release" button.
28 | 3. Create a new tag.
29 | 4. Write a title and description.
30 | 5. Publish the release.
31 | 6. PyInstaller applications will now be automatically built.
32 |
33 | GitHub actions have been defined to automatically build PyInstaller applications on Windows, macOS, and Linux; and to create a ZIP file of the QGIS plugin. These run automatically when a new tag is defined. The defined workflows will upload their files to an existing release. This means tags should only be created via the GitHub "Draft a new release" functionality, or no release will be available to upload to.
34 |
35 | ### Upload the plugin to the QGIS respository
36 |
37 | Login to the [QGIS plugin repository](https://plugins.qgis.org/plugins/qgistim/) and upload the ZIP file of the QGIS plugin.
38 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/polygon_area_sink.py:
--------------------------------------------------------------------------------
1 | from copy import deepcopy
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
5 | from qgistim.core.elements.colors import GREEN, TRANSPARENT_GREEN
6 | from qgistim.core.elements.element import Element
7 | from qgistim.core.elements.schemata import RowWiseSchema
8 | from qgistim.core.schemata import Positive, Required
9 |
10 |
11 | class PolygonAreaSinkSchema(RowWiseSchema):
12 | timml_schemata = {
13 | "geometry": Required(),
14 | "rate": Required(),
15 | "order": Required(Positive()),
16 | "ndegrees": Required(Positive()),
17 | }
18 |
19 |
20 | class PolygonAreaSink(Element):
21 | element_type = "Polygon Area Sink"
22 | geometry_type = "Polygon"
23 | timml_attributes = (
24 | QgsField("rate", QVariant.Double),
25 | QgsField("order", QVariant.Int),
26 | QgsField("ndegrees", QVariant.Int),
27 | )
28 | timml_defaults = {
29 | "order": QgsDefaultValue("4"),
30 | "ndegrees": QgsDefaultValue("6"),
31 | }
32 | schema = PolygonAreaSinkSchema()
33 |
34 | @classmethod
35 | def renderer(cls) -> QgsSingleSymbolRenderer:
36 | return cls.polygon_renderer(
37 | color=TRANSPARENT_GREEN, color_border=GREEN, width_border="0.75"
38 | )
39 |
40 | def process_timml_row(self, row, other):
41 | raw_data = deepcopy(other["global_aquifer"])
42 | raw_data["aquitard_c"][0] = None
43 | raw_data["semiconf_top"][0] = None
44 | raw_data["semiconf_head"][0] = None
45 | aquifer_data = self.aquifer_data(raw_data, transient=False)
46 | return {
47 | "xy": self.polygon_xy(row),
48 | "order": row["order"],
49 | "ndeg": row["ndegrees"],
50 | "N": row["rate"],
51 | **aquifer_data,
52 | }
53 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/impermeable_line_doublet.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
3 | from qgistim.core.elements.colors import RED
4 | from qgistim.core.elements.element import Element
5 | from qgistim.core.elements.schemata import RowWiseSchema
6 | from qgistim.core.schemata import Membership, Positive, Required
7 |
8 |
9 | class ImpermeableLineDoubletSchema(RowWiseSchema):
10 | timml_schemata = {
11 | "geometry": Required(),
12 | "order": Required(Positive()),
13 | "layer": Required(Membership("aquifer layers")),
14 | }
15 |
16 |
17 | class ImpermeableLineDoublet(Element):
18 | element_type = "Impermeable Line Doublet"
19 | geometry_type = "Linestring"
20 | timml_attributes = (
21 | QgsField("order", QVariant.Int),
22 | QgsField("layer", QVariant.Int),
23 | QgsField("label", QVariant.String),
24 | )
25 | timml_defaults = {
26 | "order": QgsDefaultValue("4"),
27 | }
28 | schema = ImpermeableLineDoubletSchema()
29 |
30 | @classmethod
31 | def renderer(cls) -> QgsSingleSymbolRenderer:
32 | return cls.line_renderer(color=RED, width="0.75")
33 |
34 | def process_timml_row(self, row, other=None):
35 | return {
36 | "xy": self.linestring_xy(row),
37 | "layers": row["layer"],
38 | "order": row["order"],
39 | "label": row["label"],
40 | }
41 |
42 | def to_ttim(self, other):
43 | # TTim doesn't have an ImpermeableLineDoublet, we need to add "imp" as
44 | # the resistance entry.
45 | _, data = self.to_timml(other)
46 | out = []
47 | for row in data:
48 | out.append(
49 | {
50 | "xy": row["xy"],
51 | "res": "imp",
52 | "order": row["order"],
53 | "label": row["label"],
54 | }
55 | )
56 | return None, out
57 |
--------------------------------------------------------------------------------
/plugin/qgistim/qgistim.py:
--------------------------------------------------------------------------------
1 | """
2 | Setup a dockwidget to hold the qgistim plugin widgets.
3 | """
4 | from pathlib import Path
5 |
6 | from qgis.gui import QgsDockWidget
7 | from qgis.PyQt.QtCore import Qt
8 | from qgis.PyQt.QtGui import QIcon
9 | from qgis.PyQt.QtWidgets import QAction
10 |
11 |
12 | class TimDockWidget(QgsDockWidget):
13 | def closeEvent(self, event) -> None:
14 | """
15 | Make sure the external interpreter is shutdown as well.
16 | """
17 | widget = self.widget()
18 | widget.shutdown_server()
19 | event.accept()
20 |
21 |
22 | class QgisTimPlugin:
23 | def __init__(self, iface):
24 | # Save reference to the QGIS interface
25 | self.iface = iface
26 | self.tim_widget = None
27 | self.plugin_dir = Path(__file__).parent
28 | self.pluginIsActive = False
29 | self.toolbar = iface.addToolBar("QgisTim")
30 | self.toolbar.setObjectName("QgisTim")
31 | return
32 |
33 | def add_action(self, icon_name, text="", callback=None, add_to_menu=False):
34 | icon = QIcon(str(self.plugin_dir / icon_name))
35 | action = QAction(icon, text, self.iface.mainWindow())
36 | action.triggered.connect(callback)
37 | if add_to_menu:
38 | self.toolbar.addAction(action)
39 | return action
40 |
41 | def initGui(self):
42 | icon_name = "icon.png"
43 | self.action_timml = self.add_action(
44 | icon_name, "QGIS-Tim", self.toggle_timml, True
45 | )
46 |
47 | def toggle_timml(self):
48 | if self.tim_widget is None:
49 | from .widgets.tim_widget import QgisTimWidget
50 |
51 | self.tim_widget = TimDockWidget("QGIS-Tim")
52 | self.tim_widget.setObjectName("QgisTimDock")
53 | self.iface.addDockWidget(Qt.RightDockWidgetArea, self.tim_widget)
54 | widget = QgisTimWidget(self.tim_widget, self.iface)
55 | self.tim_widget.setWidget(widget)
56 | self.tim_widget.hide()
57 | self.tim_widget.setVisible(not self.tim_widget.isVisible())
58 |
59 | def unload(self):
60 | self.toolbar.deleteLater()
61 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/polygon_semi_confined_top.py:
--------------------------------------------------------------------------------
1 | from copy import deepcopy
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
5 | from qgistim.core.elements.colors import BLUE, TRANSPARENT_BLUE
6 | from qgistim.core.elements.element import Element
7 | from qgistim.core.elements.schemata import RowWiseSchema
8 | from qgistim.core.schemata import Positive, Required, StrictlyPositive
9 |
10 |
11 | class PolygonSemiConfinedTopSchema(RowWiseSchema):
12 | timml_schemata = {
13 | "geometry": Required(),
14 | "aquitard_c": Required(StrictlyPositive()),
15 | "semiconf_top": Required(),
16 | "semiconf_head": Required(),
17 | "order": Required(Positive()),
18 | "ndegrees": Required(Positive()),
19 | }
20 |
21 |
22 | class PolygonSemiConfinedTop(Element):
23 | element_type = "Polygon Semi-Confined Top"
24 | geometry_type = "Polygon"
25 | timml_attributes = (
26 | QgsField("aquitard_c", QVariant.Double),
27 | QgsField("semiconf_top", QVariant.Double),
28 | QgsField("semiconf_head", QVariant.Double),
29 | QgsField("order", QVariant.Int),
30 | QgsField("ndegrees", QVariant.Int),
31 | )
32 | timml_defaults = {
33 | "order": QgsDefaultValue("4"),
34 | "ndegrees": QgsDefaultValue("6"),
35 | }
36 | schema = PolygonSemiConfinedTopSchema()
37 |
38 | @classmethod
39 | def renderer(cls) -> QgsSingleSymbolRenderer:
40 | return cls.polygon_renderer(
41 | color=TRANSPARENT_BLUE, color_border=BLUE, width_border="0.75"
42 | )
43 |
44 | def process_timml_row(self, row, other):
45 | raw_data = deepcopy(other["global_aquifer"])
46 | raw_data["aquitard_c"][0] = row["aquitard_c"]
47 | raw_data["semiconf_top"][0] = row["semiconf_top"]
48 | raw_data["semiconf_head"][0] = row["semiconf_head"]
49 | aquifer_data = self.aquifer_data(raw_data, transient=False)
50 | return {
51 | "xy": self.polygon_xy(row),
52 | "order": row["order"],
53 | "ndeg": row["ndegrees"],
54 | **aquifer_data,
55 | }
56 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/observation.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
3 | from qgistim.core.elements.colors import LIGHT_BLUE
4 | from qgistim.core.elements.element import TransientElement
5 | from qgistim.core.elements.schemata import RowWiseSchema
6 | from qgistim.core.schemata import (
7 | AllRequired,
8 | Membership,
9 | Positive,
10 | Required,
11 | StrictlyIncreasing,
12 | )
13 |
14 |
15 | class HeadObservationSchema(RowWiseSchema):
16 | timml_schemata = {
17 | "geometry": Required(),
18 | }
19 | ttim_schemata = {"timeseries_id": Required(Membership("ttim timeseries IDs"))}
20 | timeseries_schemata = {
21 | "timeseries_id": AllRequired(),
22 | "time": AllRequired(Positive(), StrictlyIncreasing()),
23 | }
24 |
25 |
26 | class HeadObservation(TransientElement):
27 | element_type = "Head Observation"
28 | geometry_type = "Point"
29 | timml_attributes = (
30 | QgsField("label", QVariant.String),
31 | QgsField("timeseries_id", QVariant.Int),
32 | )
33 | ttim_attributes = (
34 | QgsField("timeseries_id", QVariant.Int),
35 | QgsField("time", QVariant.Double),
36 | )
37 | timml_defaults = {
38 | "timeseries_id": QgsDefaultValue("1"),
39 | }
40 | ttim_defaults = {
41 | "timeseries_id": QgsDefaultValue("1"),
42 | }
43 | transient_columns = ("timeseries_id",)
44 | schema = HeadObservationSchema()
45 |
46 | @classmethod
47 | def renderer(cls) -> QgsSingleSymbolRenderer:
48 | return cls.marker_renderer(color=LIGHT_BLUE, name="triangle", size="3")
49 |
50 | def process_timml_row(self, row, other=None):
51 | x, y = self.point_xy(row)
52 | return {
53 | "x": x,
54 | "y": y,
55 | "label": row["label"],
56 | }
57 |
58 | def process_ttim_row(self, row, grouped):
59 | x, y = self.point_xy(row)
60 | times = grouped[row["timeseries_id"]]["time"]
61 | return {
62 | "x": x,
63 | "y": y,
64 | "t": times,
65 | "label": row["label"],
66 | }, times
67 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/task.py:
--------------------------------------------------------------------------------
1 | import abc
2 |
3 | from qgis.core import Qgis, QgsTask
4 |
5 |
6 | class BaseServerTask(QgsTask):
7 | def __init__(self, parent, data, message_bar):
8 | super().__init__(self.task_description, QgsTask.CanCancel)
9 | self.parent = parent
10 | self.data = data
11 | self.message_bar = message_bar
12 | self.response = None
13 | self.exception = None
14 |
15 | def run(self):
16 | try:
17 | self.response = self.parent.parent.execute(self.data)
18 | if self.response["success"]:
19 | return True
20 | else:
21 | return False
22 | except Exception as exception:
23 | self.exception = exception
24 | return False
25 |
26 | @abc.abstractproperty
27 | def task_description(self):
28 | return
29 |
30 | @abc.abstractmethod
31 | def success_message(self):
32 | return f"Completed {self.task_description}"
33 |
34 | def push_success_message(self) -> None:
35 | self.message_bar.pushMessage(
36 | title="Info",
37 | text=self.success_message(),
38 | level=Qgis.Info,
39 | )
40 | return
41 |
42 | def push_failure_message(self) -> None:
43 | if self.exception is not None:
44 | message = "Exception: " + str(self.exception)
45 | elif self.response is not None:
46 | message = "Response: " + self.response["message"]
47 | else:
48 | message = "Unknown failure"
49 |
50 | self.message_bar.pushMessage(
51 | title="Error",
52 | text=f"Failed {self.task_description}. Server error:\n{message}",
53 | level=Qgis.Critical,
54 | )
55 | return
56 |
57 | def finished(self, result) -> None:
58 | self.parent.set_interpreter_interaction(True)
59 | if result:
60 | self.push_success_message()
61 | else:
62 | self.push_failure_message()
63 | return
64 |
65 | def cancel(self) -> None:
66 | self.parent.set_interpreter_interaction(True)
67 | self.parent.shutdown_server()
68 | super().cancel()
69 | return
70 |
--------------------------------------------------------------------------------
/docs/_extensions/quarto-ext/fontawesome/fontawesome.lua:
--------------------------------------------------------------------------------
1 | local function ensureLatexDeps()
2 | quarto.doc.use_latex_package("fontawesome5")
3 | end
4 |
5 | local function ensureHtmlDeps()
6 | quarto.doc.add_html_dependency({
7 | name = 'fontawesome6',
8 | version = '0.1.0',
9 | stylesheets = {'assets/css/all.css', 'assets/css/latex-fontsize.css'}
10 | })
11 | end
12 |
13 | local function isEmpty(s)
14 | return s == nil or s == ''
15 | end
16 |
17 | local function isValidSize(size)
18 | local validSizes = {
19 | "tiny",
20 | "scriptsize",
21 | "footnotesize",
22 | "small",
23 | "normalsize",
24 | "large",
25 | "Large",
26 | "LARGE",
27 | "huge",
28 | "Huge"
29 | }
30 | for _, v in ipairs(validSizes) do
31 | if v == size then
32 | return size
33 | end
34 | end
35 | return ""
36 | end
37 |
38 | return {
39 | ["fa"] = function(args, kwargs)
40 |
41 | local group = "solid"
42 | local icon = pandoc.utils.stringify(args[1])
43 | if #args > 1 then
44 | group = icon
45 | icon = pandoc.utils.stringify(args[2])
46 | end
47 |
48 | local title = pandoc.utils.stringify(kwargs["title"])
49 | if not isEmpty(title) then
50 | title = " title=\"" .. title .. "\""
51 | end
52 |
53 | local label = pandoc.utils.stringify(kwargs["label"])
54 | if isEmpty(label) then
55 | label = " aria-label=\"" .. icon .. "\""
56 | else
57 | label = " aria-label=\"" .. label .. "\""
58 | end
59 |
60 | local size = pandoc.utils.stringify(kwargs["size"])
61 |
62 | -- detect html (excluding epub which won't handle fa)
63 | if quarto.doc.is_format("html:js") then
64 | ensureHtmlDeps()
65 | if not isEmpty(size) then
66 | size = " fa-" .. size
67 | end
68 | return pandoc.RawInline(
69 | 'html',
70 | ""
71 | )
72 | -- detect pdf / beamer / latex / etc
73 | elseif quarto.doc.is_format("pdf") then
74 | ensureLatexDeps()
75 | if isEmpty(isValidSize(size)) then
76 | return pandoc.RawInline('tex', "\\faIcon{" .. icon .. "}")
77 | else
78 | return pandoc.RawInline('tex', "{\\" .. size .. "\\faIcon{" .. icon .. "}}")
79 | end
80 | else
81 | return pandoc.Null()
82 | end
83 | end
84 | }
85 |
--------------------------------------------------------------------------------
/plugin/qgistim/widgets/error_window.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from PyQt5.QtWidgets import QDialog, QHBoxLayout, QPushButton, QTextBrowser, QVBoxLayout
4 |
5 |
6 | def format_list(errors: Dict[str, Any]):
7 | """
8 | Format the a list of errors to HTML lists.
9 |
10 | Since the level of nesting may vary, this function is called recursively.
11 | """
12 | messages = []
13 | for variable, var_errors in errors.items():
14 | if isinstance(var_errors, list):
15 | messages.append(f"
{variable}
")
16 | messages.extend(f"- {error}
" for error in var_errors)
17 | messages.append("
")
18 | else:
19 | messages.append(f"{variable}
- ")
20 | messages.extend(format_list(var_errors))
21 | messages.append("
")
22 | return messages
23 |
24 |
25 | def format_errors(errors: Dict[str, Dict[str, Any]]):
26 | """Format the errors per element to HTML lists."""
27 | messages = []
28 | for element, element_errors in errors.items():
29 | messages.append(f"{element}")
30 | messages.extend(format_list(element_errors))
31 | return "".join(messages)
32 |
33 |
34 | class ValidationDialog(QDialog):
35 | """
36 | Presents the result of the validation by the schemata in an HTML window.
37 |
38 | A QTextBrowser is used here since it has some useful properties:
39 |
40 | * It features an automatic scrollbar.
41 | * It uses HTML, so we may show nested lists.
42 | """
43 |
44 | def __init__(self, errors: Dict[str, Dict[str, Any]]):
45 | super().__init__()
46 | self.cancel_button = QPushButton("Close")
47 | self.cancel_button.clicked.connect(self.reject)
48 | self.textbox = QTextBrowser()
49 | self.textbox.setReadOnly(True)
50 | self.textbox.setHtml(format_errors(errors))
51 | first_row = QHBoxLayout()
52 | first_row.addWidget(self.textbox)
53 | second_row = QHBoxLayout()
54 | second_row.addStretch()
55 | second_row.addWidget(self.cancel_button)
56 | layout = QVBoxLayout()
57 | layout.addLayout(first_row)
58 | layout.addLayout(second_row)
59 | self.setLayout(layout)
60 | self.setWindowTitle("Invalid model input")
61 | self.textbox.setMinimumWidth(500)
62 | self.textbox.setMinimumHeight(500)
63 | self.show()
64 |
--------------------------------------------------------------------------------
/test_plugin/test_aquifer_schemata.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from qgistim.core.elements.aquifer import AquiferSchema
4 |
5 |
6 | class TestAquiferSchema(TestCase):
7 | def test_validate(self):
8 | schema = AquiferSchema()
9 | data = {
10 | "layer": [0],
11 | "aquifer_top": [10.0],
12 | "aquifer_bottom": [0.0],
13 | "aquitard_c": [None],
14 | "aquifer_k": [5.0],
15 | "semiconf_top": [None],
16 | "semiconf_head": [None],
17 | }
18 | self.assertEqual(schema.validate_timml(data), {})
19 |
20 | def test_validate_empty(self):
21 | schema = AquiferSchema()
22 | data = {}
23 | self.assertEqual(schema.validate_timml(data), {"Table:": ["Table is empty."]})
24 |
25 | def test_validate_two_layer(self):
26 | schema = AquiferSchema()
27 | data = {
28 | "layer": [0, 1],
29 | "aquifer_top": [10.0, -5.0],
30 | "aquifer_bottom": [0.0, -15.0],
31 | "aquitard_c": [None, 100.0],
32 | "aquifer_k": [5.0, 10.0],
33 | "semiconf_top": [None],
34 | "semiconf_head": [None],
35 | }
36 | self.assertEqual(schema.validate_timml(data), {})
37 |
38 | def test_validate_two_layer_invalid(self):
39 | schema = AquiferSchema()
40 | data = {
41 | "layer": [0, 1],
42 | "aquifer_top": [10.0, -5.0],
43 | "aquifer_bottom": [0.0, -15.0],
44 | "aquitard_c": [None, None],
45 | "aquifer_k": [5.0, 10.0],
46 | "semiconf_top": [None],
47 | "semiconf_head": [None],
48 | }
49 | expected = {"aquitard_c": ["No values provided at row(s): 2"]}
50 | self.assertEqual(schema.validate_timml(data), expected)
51 |
52 | def test_validate_two_layer_consistency(self):
53 | schema = AquiferSchema()
54 | data = {
55 | "layer": [0, 1],
56 | "aquifer_top": [9.0, -15.0],
57 | "aquifer_bottom": [10.0, -5.0],
58 | "aquitard_c": [None, 10.0],
59 | "aquifer_k": [5.0, 10.0],
60 | "semiconf_top": [None],
61 | "semiconf_head": [None],
62 | }
63 | expected = {
64 | "Table:": [
65 | "aquifer_top is not greater or equal to aquifer_bottom at row(s): 1, 2"
66 | ]
67 | }
68 | self.assertEqual(schema.validate_timml(data), expected)
69 |
--------------------------------------------------------------------------------
/docs/deltaforge_install.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: Install iMOD Python with Deltaforge
3 | ---
4 |
5 | ## What is Deltaforge?
6 |
7 | Deltaforge is a python distribution which includes iMOD Python and all
8 | its dependencies. It is provided as an installer and makes installing
9 | iMOD Python easy. You can download the Deltaforge installer [on the Deltares download
10 | portal](https://download.deltares.nl/imod-suite/).
11 |
12 | ## Installation
13 |
14 | To install Deltaforge, double-click the executable, this will open the
15 | installation Wizard. You will be greeted with the welcome screen.
16 |
17 | 
18 |
19 | Click \"Next\", and then \"I agree\" in the license agreement.
20 |
21 | 
22 |
23 | Next, you get to decide what type of installation you want. On your
24 | local machine it suffices to select [Just me]{.title-ref}. If you are an
25 | admin of a server and you want to let others enjoy the Deltaforge
26 | installation as well, click [All Users]{.title-ref}.
27 |
28 | 
30 |
31 | Next you get to decide where the python environment is installed. The
32 | default location is usually fine.
33 |
34 | 
36 |
37 | Finally, some further configuration is possible. The screenshots
38 | contains the options we recommend.
39 |
40 | 
42 |
43 | ## Using Deltaforge
44 |
45 | The easiest way to start your environment is by pressing the Windows Key
46 | and start typing [deltaforge]{.title-ref}. This will let you select the
47 | [Deltaforge Prompt]{.title-ref}. Select this.
48 |
49 | 
51 |
52 | This will start a command prompt screen (`cmd.exe`), where at startup
53 | the Deltaforge python environment is activated.
54 |
55 | 
57 |
58 | To view all the packages installed in the environment you can type
59 | `mamba list` and press Enter. This will list all packages installed in
60 | the environment. If you want to start coding, you can type `spyder`,
61 | which will start Spyder, a Python scientific development environment.
62 |
--------------------------------------------------------------------------------
/plugin/qgistim/qt/qgistim_name_dialog_base.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | LayerNameDialogBase
4 |
5 |
6 |
7 | 0
8 | 0
9 | 365
10 | 72
11 |
12 |
13 |
14 | TimML Element
15 |
16 |
17 |
18 |
19 | 190
20 | 40
21 | 156
22 | 24
23 |
24 |
25 |
26 | Qt::Horizontal
27 |
28 |
29 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
30 |
31 |
32 |
33 |
34 |
35 | 14
36 | 10
37 | 331
38 | 31
39 |
40 |
41 |
42 | -
43 |
44 |
45 | Layer name
46 |
47 |
48 |
49 | -
50 |
51 |
52 |
53 | 50
54 | 0
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | button_box
66 | accepted()
67 | LayerNameDialogBase
68 | accept()
69 |
70 |
71 | 20
72 | 20
73 |
74 |
75 | 20
76 | 20
77 |
78 |
79 |
80 |
81 | button_box
82 | rejected()
83 | LayerNameDialogBase
84 | reject()
85 |
86 |
87 | 20
88 | 20
89 |
90 |
91 | 20
92 | 20
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/plugin/qgistim/qt/qgistim_conda_env.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | InterpreterDialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 400
10 | 108
11 |
12 |
13 |
14 | Select environment
15 |
16 |
17 |
18 |
19 | 10
20 | 10
21 | 381
22 | 91
23 |
24 |
25 |
26 | -
27 |
28 |
29 | -
30 |
31 |
-
32 |
33 |
34 | Add interpreter:
35 |
36 |
37 |
38 | -
39 |
40 |
41 | ...
42 |
43 |
44 |
45 |
46 |
47 | -
48 |
49 |
50 | Qt::Horizontal
51 |
52 |
53 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | buttonBox
64 | rejected()
65 | InterpreterDialog
66 | reject()
67 |
68 |
69 | 316
70 | 260
71 |
72 |
73 | 286
74 | 274
75 |
76 |
77 |
78 |
79 | buttonBox
80 | accepted()
81 | InterpreterDialog
82 | accept()
83 |
84 |
85 | 248
86 | 254
87 |
88 |
89 | 157
90 | 274
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/install.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Install"
3 | ---
4 |
5 | QGIS-Tim consists of two parts: A QGIS plugin and the ``gistim`` Python
6 | package, which runs in a separate Python environment. The installation of
7 | QGIS-Tim therfore consists of three steps:
8 |
9 | ## 1. Install QGIS
10 |
11 | Download and install a recent version of QGIS (>=3.28):
12 | {target="_blank"}
13 |
14 | ## 2. Install the QGIS plugin
15 |
16 | ### Method A: From the QGIS plugin database (recommended)
17 |
18 | 1. Open QGIS.
19 | 3. At the top, find the Plugins menu (\~sixth object in the menubar).
20 | 4. Find \"Manage and Install plugins\" (\~first object in drop-down).
21 | 5. Find \"All\" (\~first in left section).
22 | 6. Search for \"Qgis-Tim\".
23 | 7. Click \"Install Plugin\".
24 |
25 | ### Method B: From ZIP file
26 | 1. Download the \"QGIS-Tim-plugin.zip\" from the [GitHub Releases page](https://github.com/Deltares/QGIS-Tim/releases) (do not unzip!).
27 | 2. Open QGIS.
28 | 3. At the top, find the Plugins menu (\~sixth object in the menubar).
29 | 4. Find \"Manage and Install plugins\" (\~first object in drop-down).
30 | 5. Find \"Install from ZIP\" (\~fourth in left section).
31 | 6. Enter the path to the file \"QGIS-TIM-plugin.zip\".
32 | 7. Click \"Install Plugin\".
33 |
34 | This will add an icon to the toolbar(s). {width=6%}
35 |
36 | By clicking the icon, the plugin is started.
37 |
38 | ## 3. Install the TimML and TTim server
39 | With the plugin installed, we can already define model input and convert it to Python scripts or JSON files.
40 | To run TimML and TTim computations directly from QGIS, we need to install a server program which contains TimML and TTim.
41 |
42 | ### Method A: Install from GitHub (requires internet connection)
43 |
44 | 1. Start the QGIS-Tim plugin by clicking the QGIS-Tim icon in the toolbar.
45 | 1. Find and click the "Install TimML and TTim server" button at the bottom of the plugin window.
46 | 1. Click the "Install latest release from GitHub" button to download and install the server program.
47 |
48 | ### Method B: Install from ZIP file
49 |
50 | Specific releases can also be manually downloaded from the [GitHub Releases page](https://github.com/Deltares/QGIS-Tim/releases):
51 |
52 | 1. Download the gistim ZIP file for your platform: Windows, macOS, or Linux.
53 | 1. Start the QGIS-Tim plugin by clicking the QGIS-Tim icon in the toolbar.
54 | 1. Find and click the "Install TimML and TTim server" button at the bottom of the plugin window.
55 | 1. Set the path to the downloaded ZIP file in the "Install from ZIP file" section.
56 | 1. Click the "Install" button.
57 |
--------------------------------------------------------------------------------
/plugin/qgistim/widgets/elements_widget.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 |
3 | from PyQt5.QtWidgets import QGridLayout, QPushButton, QVBoxLayout, QWidget
4 | from qgis.core import Qgis
5 | from qgistim.core.elements import ELEMENTS
6 |
7 |
8 | class ElementsWidget(QWidget):
9 | def __init__(self, parent):
10 | super().__init__(parent)
11 | self.parent = parent
12 |
13 | self.element_buttons = {}
14 | for element in ELEMENTS:
15 | if element in ("Aquifer", "Domain"):
16 | continue
17 | button = QPushButton(element)
18 | button.clicked.connect(partial(self.tim_element, element_type=element))
19 | button.setEnabled(False)
20 | self.element_buttons[element] = button
21 |
22 | elements_layout = QVBoxLayout()
23 | elements_grid = QGridLayout()
24 | n_row = -(len(self.element_buttons) // -2) # Ceiling division
25 | for i, button in enumerate(self.element_buttons.values()):
26 | if i < n_row:
27 | elements_grid.addWidget(button, i, 0)
28 | else:
29 | elements_grid.addWidget(button, i % n_row, 1)
30 | elements_layout.addLayout(elements_grid)
31 | elements_layout.addStretch()
32 | self.setLayout(elements_layout)
33 |
34 | def enable_element_buttons(self) -> None:
35 | """
36 | Enables or disables the element buttons.
37 |
38 | Parameters
39 | ----------
40 | state: bool
41 | True to enable, False to disable
42 | """
43 | for button in self.element_buttons.values():
44 | button.setEnabled(True)
45 |
46 | def tim_element(self, element_type: str) -> None:
47 | """
48 | Create a new TimML element input layer.
49 |
50 | Parameters
51 | ----------
52 | element_type: str
53 | Name of the element type.
54 | """
55 | klass = ELEMENTS[element_type]
56 | names = self.parent.selection_names()
57 |
58 | # Get the crs. If not a CRS in meters, abort.
59 | try:
60 | crs = self.parent.crs
61 | except ValueError:
62 | return
63 |
64 | try:
65 | element = klass.dialog(self.parent.path, crs, self.parent.iface, names)
66 | except ValueError as e:
67 | msg = str(e)
68 | self.parent.message_bar.pushMessage("Error", msg, level=Qgis.Critical)
69 | return
70 |
71 | if element is None: # dialog cancelled
72 | return
73 | # Write to geopackage
74 | element.write()
75 | # Add to QGIS and dataset tree
76 | self.parent.add_element(element)
77 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/install_backend.py:
--------------------------------------------------------------------------------
1 | """Downloads and installs from GitHub or installs from zipfile."""
2 | from typing import Dict
3 | from pathlib import Path
4 | from zipfile import ZipFile
5 | from io import BytesIO
6 | import json
7 | import requests
8 | import hashlib
9 | import platform
10 | import shutil
11 | import os
12 |
13 |
14 | def get_gistim_dir() -> Path:
15 | if platform.system() == "Windows":
16 | gistim_dir = Path(os.environ["APPDATA"]) / "qgis-tim"
17 | else:
18 | gistim_dir = Path(os.environ["HOME"]) / ".qgis-tim"
19 | return gistim_dir
20 |
21 |
22 | def get_release_assets() -> Dict[str, str]:
23 | GITHUB_URL = "https://api.github.com/repos/deltares/qgis-tim/releases"
24 | response = requests.get(GITHUB_URL)
25 | json_content = json.loads(response.content)
26 | last_release = json_content[0]
27 | assets = last_release["assets"]
28 | named_assets = {asset["name"]: asset["browser_download_url"] for asset in assets}
29 | return named_assets
30 |
31 |
32 | def download_assets(assets: Dict[str, str]) -> ZipFile:
33 | SYSTEMS = {
34 | "Windows": "Windows",
35 | "Darwin": "macOS",
36 | "Linux": "Linux",
37 | }
38 | user_system = platform.system()
39 | github_system = SYSTEMS.get(user_system)
40 | if github_system is None:
41 | raise ValueError(
42 | f"Unsupported OS: {user_system}. "
43 | f"Only {', '.join(SYSTEMS.keys())} are supported."
44 | )
45 | # Get checksum
46 | checksum_url = assets[f"sha256-checksum-{github_system}.txt"]
47 | checksum_github = requests.get(checksum_url).content.decode("utf-8")
48 | # Get zipfile content
49 | zip_url = assets[f"gistim-{github_system}.zip"]
50 | zipfile_content = requests.get(zip_url).content
51 | # Compare checksums
52 | sha = hashlib.sha256()
53 | sha.update(zipfile_content)
54 | checksum = sha.hexdigest()
55 | if checksum != checksum_github:
56 | raise ValueError("SHA-256 checksums do not match. Please try re-downloading.")
57 | return ZipFile(BytesIO(zipfile_content))
58 |
59 |
60 | def create_destination() -> Path:
61 | gistim_dir = get_gistim_dir()
62 | if gistim_dir.exists():
63 | shutil.rmtree(gistim_dir)
64 | gistim_dir.mkdir()
65 | return gistim_dir
66 |
67 |
68 | def install_from_github() -> None:
69 | assets = get_release_assets()
70 | zipfile = download_assets(assets)
71 | destination = create_destination()
72 | zipfile.extractall(destination)
73 | return
74 |
75 |
76 | def install_from_zip(path: str) -> None:
77 | if not Path(path).exists:
78 | raise FileNotFoundError(path)
79 | zipfile = ZipFile(path)
80 | destination = create_destination()
81 | zipfile.extractall(destination)
82 | return
83 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/geopackage.py:
--------------------------------------------------------------------------------
1 | """
2 | Geopackage management utilities.
3 |
4 | This module lightly wraps a few QGIS built in functions to:
5 |
6 | * List the layers of a geopackage
7 | * Write a layer to a geopackage
8 | * Remove a layer from a geopackage
9 |
10 | """
11 | import sqlite3
12 | from contextlib import contextmanager
13 | from typing import List
14 |
15 | from qgis import processing
16 | from qgis.core import QgsVectorFileWriter, QgsVectorLayer
17 |
18 |
19 | @contextmanager
20 | def sqlite3_cursor(path):
21 | connection = sqlite3.connect(path)
22 | cursor = connection.cursor()
23 | try:
24 | yield cursor
25 | finally:
26 | cursor.close()
27 | connection.close()
28 |
29 |
30 | def layers(path: str) -> List[str]:
31 | """
32 | Return all layers that are present in the geopackage.
33 |
34 | Parameters
35 | ----------
36 | path: str
37 | Path to the geopackage
38 |
39 | Returns
40 | -------
41 | layernames: List[str]
42 | """
43 | with sqlite3_cursor(path) as cursor:
44 | cursor.execute("Select table_name from gpkg_contents")
45 | layers = [item[0] for item in cursor.fetchall()]
46 | return layers
47 |
48 |
49 | def write_layer(
50 | path: str, layer: QgsVectorLayer, layername: str, newfile: bool = False
51 | ) -> QgsVectorLayer:
52 | """
53 | Writes a QgsVectorLayer to a GeoPackage file.
54 |
55 | Parameters
56 | ----------
57 | path: str
58 | Path to the GeoPackage file
59 | layer: QgsVectorLayer
60 | QGIS map layer (in-memory)
61 | layername: str
62 | Layer name to write in the GeoPackage
63 | newfile: bool, optional
64 | Whether to write a new GeoPackage file. Defaults to false.
65 |
66 | Returns
67 | -------
68 | layer: QgsVectorLayer
69 | The layer, now associated with the both GeoPackage and its QGIS
70 | representation.
71 | """
72 | options = QgsVectorFileWriter.SaveVectorOptions()
73 | options.driverName = "gpkg"
74 | options.layerName = layername
75 | if not newfile:
76 | options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
77 | write_result, error_message = QgsVectorFileWriter.writeAsVectorFormat(
78 | layer, path, options
79 | )
80 | if write_result != QgsVectorFileWriter.NoError:
81 | raise RuntimeError(
82 | f"Layer {layername} could not be written to geopackage: {path}"
83 | f" with error: {error_message}"
84 | )
85 | layer = QgsVectorLayer(f"{path}|layername={layername}", layername, "ogr")
86 | return layer
87 |
88 |
89 | def remove_layer(path: str, layer: str) -> None:
90 | query = {"DATABASE": f"{path}|layername={layer}", "SQL": f"drop table {layer}"}
91 | try:
92 | processing.run("native:spatialiteexecutesql", query)
93 | except Exception:
94 | raise RuntimeError(f"Failed to remove layer with {query}")
95 |
--------------------------------------------------------------------------------
/plugin/qgistim/qt/qgistim_radius_dialog_base.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 368
10 | 99
11 |
12 |
13 |
14 | Circular Area Sink
15 |
16 |
17 |
18 |
19 | 160
20 | 70
21 | 193
22 | 28
23 |
24 |
25 |
26 | Qt::Horizontal
27 |
28 |
29 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
30 |
31 |
32 |
33 |
34 |
35 | 20
36 | 10
37 | 331
38 | 31
39 |
40 |
41 |
42 | -
43 |
44 |
45 | Layer name
46 |
47 |
48 |
49 | -
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 20
58 | 40
59 | 331
60 | 31
61 |
62 |
63 |
64 | -
65 |
66 |
67 | Radius
68 |
69 |
70 |
71 | -
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | buttonBox
81 | accepted()
82 | Dialog
83 | accept()
84 |
85 |
86 | 248
87 | 254
88 |
89 |
90 | 157
91 | 274
92 |
93 |
94 |
95 |
96 | buttonBox
97 | rejected()
98 | Dialog
99 | reject()
100 |
101 |
102 | 316
103 | 260
104 |
105 |
106 | 286
107 | 274
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/extractor.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from collections import defaultdict
3 | from typing import Any, Dict, List, Tuple
4 |
5 | from qgis.core import NULL, QgsVectorLayer
6 |
7 |
8 | def remove_zero_length(geometry) -> List:
9 | # This removes repeated vertices resulting in zero length segments.
10 | # These zero length segments will crash TimML.
11 | previous_pair = geometry[0]
12 | coordinates = [previous_pair]
13 | for pair in geometry[1:]:
14 | if pair != previous_pair:
15 | coordinates.append(pair)
16 | previous_pair = pair
17 | return coordinates
18 |
19 |
20 | class ExtractorMixin(abc.ABC):
21 | """
22 | Mixin class to extract all data from QgsVectorLayers.
23 | """
24 |
25 | @staticmethod
26 | def argsort(seq):
27 | return sorted(range(len(seq)), key=seq.__getitem__)
28 |
29 | @staticmethod
30 | def extract_coordinates(geometry):
31 | coordinates = []
32 | for vertex in geometry.vertices():
33 | coordinates.append((vertex.x(), vertex.y()))
34 | centroid = geometry.centroid().asPoint()
35 | return (centroid.x(), centroid.y()), coordinates
36 |
37 | @classmethod
38 | def table_to_records(cls, layer: QgsVectorLayer) -> List[Dict[str, Any]]:
39 | # layer.geometryType().Null is an enumerator, which isn't available in QGIS 3.28 LTR.
40 | # So just use the integer representation instead for now.
41 | # FUTURE: GEOM_TYPE_NULL = geomtype.Null
42 | GEOM_TYPE_NULL = 4
43 | geomtype = layer.geometryType()
44 | features = []
45 | for feature in layer.getFeatures():
46 | data = feature.attributeMap()
47 | for key, value in data.items():
48 | if value == NULL:
49 | data[key] = None
50 |
51 | if geomtype != GEOM_TYPE_NULL:
52 | geometry = feature.geometry()
53 | if geometry.isNull():
54 | centroid = None
55 | coordinates = None
56 | else:
57 | centroid, coordinates = cls.extract_coordinates(geometry)
58 | data["centroid"], data["geometry"] = centroid, coordinates
59 |
60 | features.append(data)
61 | return features
62 |
63 | def table_to_dict(cls, layer: QgsVectorLayer) -> Dict[str, Any]:
64 | features = defaultdict(list)
65 | for feature in layer.getFeatures():
66 | for key, value in feature.attributeMap().items():
67 | if value == NULL:
68 | features[key].append(None)
69 | else:
70 | features[key].append(value)
71 | return features
72 |
73 | @staticmethod
74 | def point_xy(row) -> Tuple[List[float], List[float]]:
75 | point = row["geometry"][0]
76 | return point[0], point[1]
77 |
78 | @staticmethod
79 | def linestring_xy(row) -> List:
80 | return remove_zero_length(row["geometry"])
81 |
82 | @staticmethod
83 | def polygon_xy(row) -> List:
84 | return remove_zero_length(row["geometry"])
85 |
--------------------------------------------------------------------------------
/gistim/__main__.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import os
4 | import sys
5 | from contextlib import contextmanager, redirect_stderr, redirect_stdout
6 | from os import devnull
7 |
8 | # Make sure we explicitly import besselaesnumba for pyinstaller.
9 | # It's a dynamic import inside of timml.
10 | from timml.besselaesnumba import besselaesnumba # noqa: F401
11 |
12 | import gistim
13 |
14 |
15 | @contextmanager
16 | def suppress_stdout_stderr():
17 | """A context manager that redirects stdout and stderr to devnull"""
18 | with open(devnull, "w") as fnull:
19 | with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out:
20 | yield (err, out)
21 |
22 |
23 | def write_json_stdout(data):
24 | sys.stdout.write(json.dumps(data))
25 | sys.stdout.write("\n")
26 | sys.stdout.flush()
27 |
28 |
29 | def handle(line) -> str:
30 | data = json.loads(line)
31 | operation = data.pop("operation")
32 | if operation == "compute":
33 | gistim.compute.compute(
34 | path=data["path"],
35 | transient=data["transient"],
36 | )
37 | response = "Computation of {path}".format(**data)
38 | elif operation == "process_ID":
39 | response = str(os.getpid())
40 | else:
41 | response = 'Invalid operation. Valid options are: "compute", "process_ID".'
42 |
43 | return response
44 |
45 |
46 | def serve(_) -> None:
47 | """
48 | Spin up a process listening for calls messages from the QGIS plugin.
49 | """
50 | try:
51 | write_json_stdout({"success": True, "message": "Initialized Tim server"})
52 | for line in sys.stdin:
53 | try:
54 | with suppress_stdout_stderr():
55 | message = handle(line)
56 | response = {"success": True, "message": message}
57 |
58 | except Exception as error:
59 | response = {"success": False, "message": str(error)}
60 |
61 | write_json_stdout(response)
62 |
63 | except Exception as error:
64 | write_json_stdout({"success": False, "message": str(error)})
65 |
66 |
67 | def compute(args) -> None:
68 | if args.transient is None:
69 | transient = False
70 | else:
71 | transient = args.transient[0]
72 | gistim.compute.compute(path=args.path[0], transient=transient)
73 | return
74 |
75 |
76 | if __name__ == "__main__":
77 | # Setup argparsers
78 | parser = argparse.ArgumentParser(prog="gistim")
79 | subparsers = parser.add_subparsers(help="sub-command help")
80 | parser_serve = subparsers.add_parser("serve", help="serve help")
81 | parser_compute = subparsers.add_parser("compute", help="compute help")
82 |
83 | parser_serve.set_defaults(func=serve)
84 |
85 | parser_compute.set_defaults(func=compute)
86 | parser_compute.add_argument("path", type=str, nargs=1, help="path to JSON file")
87 | parser_compute.add_argument("--transient", action=argparse.BooleanOptionalAction)
88 | parser_compute.set_defaults(transient=False)
89 |
90 | # Parse and call the appropriate function
91 | args = parser.parse_args()
92 | args.func(args)
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QGIS-Tim
2 |
3 | QGIS-Tim is an open source project for multi-layer groundwater flow
4 | simulations. QGIS-Tim provides a link between QGIS and the open source analytic
5 | element method software: [TimML (steady-state)](https://github.com/mbakker7/timml)
6 | and [TTim (transient)](https://github.com/mbakker7/ttim).
7 |
8 | The benefit of the analytic element method (AEM) is that no grid or
9 | time-stepping is required. Geohydrological features are represented by points,
10 | lines, and polygons. QGIS-Tim stores these features in a
11 | [GeoPackage](https://www.geopackage.org/).
12 |
13 | QGIS-Tim consists of a "front-end" (the QGIS plugin) and a "back-end" (the TimML and TTim server).
14 | The front-end is a QGIS plugin that provides a limited graphical interface to setup model input,
15 | visualize, and analyze model input. The back-end is a Python package. The plugin converts the
16 | GeoPackage content to a JSON file or a Python script. The back-end reads the JSON file, does the
17 | necessary computations and writes result files that are loaded back into QGIS by the plugin.
18 |
19 | ## Documentation
20 | [Find the documentation here.](https://deltares.github.io/QGIS-Tim/)
21 |
22 | ## Installation
23 |
24 | Download and install a recent version of QGIS (>=3.28):
25 |
26 |
27 | ### Method A: From the QGIS plugin database
28 | 1. Open QGIS.
29 | 3. At the top, find the Plugins menu (\~sixth object in the menubar).
30 | 4. Find \"Manage and Install plugins\" (\~first object in drop-down).
31 | 5. Find \"All\" (\~first in left section).
32 | 6. Search for \"Qgis-Tim\".
33 | 7. Click \"Install Plugin\".
34 |
35 | **NB** The latest release might not be available yet on the QGIS plugin database, as vetting a new release takes a few days. The latest release is always available via method B.
36 |
37 | ### Method B: From ZIP file
38 | 1. Download the \"QGIS-Tim-plugin.zip\" from the [GitHub Releases page](https://github.com/Deltares/QGIS-Tim/releases) (do not unzip!).
39 | 2. Open QGIS.
40 | 3. At the top, find the Plugins menu (\~sixth object in the menubar).
41 | 4. Find \"Manage and Install plugins\" (\~first object in drop-down).
42 | 5. Find \"Install from ZIP\" (\~fourth in left section).
43 | 6. Enter the path to the file \"QGIS-TIM-plugin.zip\".
44 | 7. Click \"Install Plugin\".
45 |
46 | This will add an icon to the toolbar(s). By clicking the icon, the plugin is started.
47 |
48 | ### Install the TimML and TTim server
49 | With the plugin installed, we can already define model input and convert it to Python scripts or JSON files.
50 | To run TimML and TTim computations directly from QGIS, we need to install a server program which contains TimML and TTim.
51 |
52 | 1. Start the QGIS-Tim plugin by clicking the QGIS-Tim icon in the toolbar.
53 | 2. Find and click the "Install TimML and TTim server" button at the bottom of the plugin window.
54 | 3. Click the "Install latest release from GitHub" button to download and install the server program.
55 |
56 | Specific releases can also be manually downloaded from the [GitHub Releases page](https://github.com/Deltares/QGIS-Tim/releases):
57 |
58 | 1. Download the gistim ZIP file for your platform: Windows, macOS, or Linux.
59 | 2. Find and click the "Install TimML and TTim server" button at the bottom of the plugin window.
60 | 3. Set the path to the downloaded ZIP file in the "Install from ZIP file" section.
61 | 4. Click the "Install" button.
62 |
--------------------------------------------------------------------------------
/docs/tutorial-QGIS-TIM_files/libs/quarto-html/quarto-syntax-highlighting.css:
--------------------------------------------------------------------------------
1 | /* quarto syntax highlight colors */
2 | :root {
3 | --quarto-hl-ot-color: #003B4F;
4 | --quarto-hl-at-color: #657422;
5 | --quarto-hl-ss-color: #20794D;
6 | --quarto-hl-an-color: #5E5E5E;
7 | --quarto-hl-fu-color: #4758AB;
8 | --quarto-hl-st-color: #20794D;
9 | --quarto-hl-cf-color: #003B4F;
10 | --quarto-hl-op-color: #5E5E5E;
11 | --quarto-hl-er-color: #AD0000;
12 | --quarto-hl-bn-color: #AD0000;
13 | --quarto-hl-al-color: #AD0000;
14 | --quarto-hl-va-color: #111111;
15 | --quarto-hl-bu-color: inherit;
16 | --quarto-hl-ex-color: inherit;
17 | --quarto-hl-pp-color: #AD0000;
18 | --quarto-hl-in-color: #5E5E5E;
19 | --quarto-hl-vs-color: #20794D;
20 | --quarto-hl-wa-color: #5E5E5E;
21 | --quarto-hl-do-color: #5E5E5E;
22 | --quarto-hl-im-color: #00769E;
23 | --quarto-hl-ch-color: #20794D;
24 | --quarto-hl-dt-color: #AD0000;
25 | --quarto-hl-fl-color: #AD0000;
26 | --quarto-hl-co-color: #5E5E5E;
27 | --quarto-hl-cv-color: #5E5E5E;
28 | --quarto-hl-cn-color: #8f5902;
29 | --quarto-hl-sc-color: #5E5E5E;
30 | --quarto-hl-dv-color: #AD0000;
31 | --quarto-hl-kw-color: #003B4F;
32 | }
33 |
34 | /* other quarto variables */
35 | :root {
36 | --quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
37 | }
38 |
39 | pre > code.sourceCode > span {
40 | color: #003B4F;
41 | }
42 |
43 | code span {
44 | color: #003B4F;
45 | }
46 |
47 | code.sourceCode > span {
48 | color: #003B4F;
49 | }
50 |
51 | div.sourceCode,
52 | div.sourceCode pre.sourceCode {
53 | color: #003B4F;
54 | }
55 |
56 | code span.ot {
57 | color: #003B4F;
58 | }
59 |
60 | code span.at {
61 | color: #657422;
62 | }
63 |
64 | code span.ss {
65 | color: #20794D;
66 | }
67 |
68 | code span.an {
69 | color: #5E5E5E;
70 | }
71 |
72 | code span.fu {
73 | color: #4758AB;
74 | }
75 |
76 | code span.st {
77 | color: #20794D;
78 | }
79 |
80 | code span.cf {
81 | color: #003B4F;
82 | }
83 |
84 | code span.op {
85 | color: #5E5E5E;
86 | }
87 |
88 | code span.er {
89 | color: #AD0000;
90 | }
91 |
92 | code span.bn {
93 | color: #AD0000;
94 | }
95 |
96 | code span.al {
97 | color: #AD0000;
98 | }
99 |
100 | code span.va {
101 | color: #111111;
102 | }
103 |
104 | code span.pp {
105 | color: #AD0000;
106 | }
107 |
108 | code span.in {
109 | color: #5E5E5E;
110 | }
111 |
112 | code span.vs {
113 | color: #20794D;
114 | }
115 |
116 | code span.wa {
117 | color: #5E5E5E;
118 | font-style: italic;
119 | }
120 |
121 | code span.do {
122 | color: #5E5E5E;
123 | font-style: italic;
124 | }
125 |
126 | code span.im {
127 | color: #00769E;
128 | }
129 |
130 | code span.ch {
131 | color: #20794D;
132 | }
133 |
134 | code span.dt {
135 | color: #AD0000;
136 | }
137 |
138 | code span.fl {
139 | color: #AD0000;
140 | }
141 |
142 | code span.co {
143 | color: #5E5E5E;
144 | }
145 |
146 | code span.cv {
147 | color: #5E5E5E;
148 | font-style: italic;
149 | }
150 |
151 | code span.cn {
152 | color: #8f5902;
153 | }
154 |
155 | code span.sc {
156 | color: #5E5E5E;
157 | }
158 |
159 | code span.dv {
160 | color: #AD0000;
161 | }
162 |
163 | code span.kw {
164 | color: #003B4F;
165 | }
166 |
167 | .prevent-inlining {
168 | content: "";
169 | }
170 |
171 | /*# sourceMappingURL=debc5d5d77c3f9108843748ff7464032.css.map */
172 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/server_handler.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains the logic for starting, communicating with, and killing a
3 | separate (conda) interpreter, which is running TimML and TTim.
4 |
5 | For thread safety: DO NOT INCLUDE QGIS CALLS HERE.
6 | """
7 | import json
8 | import os
9 | import platform
10 | import subprocess
11 | from pathlib import Path
12 | from typing import Any, Dict
13 |
14 |
15 | class ServerHandler:
16 | def __init__(self):
17 | self.process = None
18 |
19 | def alive(self):
20 | return self.process is not None and self.process.poll() is None
21 |
22 | @staticmethod
23 | def get_gistim_dir() -> Path:
24 | """
25 | Get the location of the qgis-tim PyInstaller executable.
26 |
27 | The location differs per OS.
28 |
29 | Returns
30 | -------
31 | gistim_dir: pathlib.Path
32 | """
33 | if platform.system() == "Windows":
34 | gistim_dir = Path(os.environ["APPDATA"]) / "qgis-tim"
35 | else:
36 | gistim_dir = Path(os.environ["HOME"]) / ".qgis-tim"
37 | return gistim_dir
38 |
39 | @staticmethod
40 | def get_interpreter() -> Path:
41 | if platform.system() == "Windows":
42 | return ServerHandler.get_gistim_dir() / "gistim.exe"
43 | else:
44 | return ServerHandler.get_gistim_dir() / "gistim"
45 |
46 | @staticmethod
47 | def versions():
48 | path = ServerHandler.get_gistim_dir() / "versions.json"
49 | if path.exists():
50 | with open(path, "r") as f:
51 | versions = json.loads(f.read())
52 | else:
53 | versions = {}
54 | return versions
55 |
56 | def start_server(self) -> Dict[str, Any]:
57 | """
58 | Starts a new PyInstaller interpreter.
59 | """
60 | interpreter = self.get_interpreter()
61 | self.process = subprocess.Popen(
62 | [interpreter, "serve"],
63 | stdin=subprocess.PIPE,
64 | stdout=subprocess.PIPE,
65 | stderr=subprocess.PIPE,
66 | text=True,
67 | )
68 | response = json.loads(self.process.stdout.readline())
69 | return response
70 |
71 | def send(self, data) -> Dict[str, Any]:
72 | """
73 | Send a data package (should be a JSON string) to the external
74 | interpreter, running gistim.
75 |
76 | Parameters
77 | ----------
78 | data: str
79 | A JSON string describing the operation and parameters
80 |
81 | Returns
82 | -------
83 | received: str
84 | Value depends on the requested operation
85 | """
86 | self.process.stdin.write(json.dumps(data))
87 | self.process.stdin.write("\n")
88 | self.process.stdin.flush()
89 | response = json.loads(self.process.stdout.readline())
90 | return response
91 |
92 | def kill(self) -> None:
93 | """
94 | Kills the external interpreter.
95 |
96 | This enables shutting down the external window when the plugin is
97 | closed.
98 | """
99 | if self.alive():
100 | try:
101 | self.process.kill()
102 | self.process = None
103 | except ConnectionRefusedError:
104 | # it's already dead
105 | pass
106 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/circular_area_sink.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import QgsField
5 | from qgistim.core.elements.colors import GREEN, TRANSPARENT_GREEN
6 | from qgistim.core.elements.element import TransientElement
7 | from qgistim.core.elements.schemata import RowWiseSchema
8 | from qgistim.core.schemata import (
9 | AllOrNone,
10 | AllRequired,
11 | CircularGeometry,
12 | Membership,
13 | NotBoth,
14 | Optional,
15 | Positive,
16 | Required,
17 | StrictlyIncreasing,
18 | )
19 |
20 |
21 | class CircularAreaSinkSchema(RowWiseSchema):
22 | timml_schemata = {
23 | "geometry": Required(CircularGeometry()),
24 | "rate": Required(),
25 | "layer": Required(Membership("aquifer layers")),
26 | }
27 | ttim_schemata = {
28 | "time_start": Optional(Positive()),
29 | "time_end": Optional(Positive()),
30 | "timeseries_id": Optional(Membership("ttim timeseries IDs")),
31 | }
32 | ttim_consistency_schemata = (
33 | AllOrNone("time_start", "time_end", "rate_transient"),
34 | NotBoth("time_start", "timeseries_id"),
35 | )
36 | timeseries_schemata = {
37 | "timeseries_id": AllRequired(),
38 | "time_start": AllRequired(Positive(), StrictlyIncreasing()),
39 | "rate": AllRequired(),
40 | }
41 |
42 |
43 | class CircularAreaSink(TransientElement):
44 | element_type = "Circular Area Sink"
45 | geometry_type = "Polygon"
46 | timml_attributes = (
47 | QgsField("rate", QVariant.Double),
48 | QgsField("layer", QVariant.Int),
49 | QgsField("label", QVariant.String),
50 | QgsField("time_start", QVariant.Double),
51 | QgsField("time_end", QVariant.Double),
52 | QgsField("rate_transient", QVariant.Double),
53 | QgsField("timeseries_id", QVariant.Int),
54 | )
55 | ttim_attributes = (
56 | QgsField("timeseries_id", QVariant.Int),
57 | QgsField("time_start", QVariant.Double),
58 | QgsField("rate", QVariant.Double),
59 | )
60 | transient_columns = (
61 | "time_start",
62 | "time_end",
63 | "rate_transient",
64 | "timeseries_id",
65 | )
66 | schema = CircularAreaSinkSchema()
67 |
68 | @classmethod
69 | def renderer(cls):
70 | return cls.polygon_renderer(
71 | color=TRANSPARENT_GREEN, color_border=GREEN, width_border="0.75"
72 | )
73 |
74 | def _centroid_and_radius(self, row):
75 | # Take the first vertex.
76 | x, y = self.point_xy(row)
77 | # Compare with the centroid to derive radius.
78 | xc, yc = row["centroid"]
79 | radius = ((x - xc) ** 2 + (y - yc) ** 2) ** 0.5
80 | return xc, yc, radius
81 |
82 | def process_timml_row(self, row, other=None) -> Dict[str, Any]:
83 | xc, yc, radius = self._centroid_and_radius(row)
84 | return {
85 | "xc": xc,
86 | "yc": yc,
87 | "R": radius,
88 | "N": row["rate"],
89 | "label": row["label"],
90 | }
91 |
92 | def process_ttim_row(self, row, grouped):
93 | xc, yc, radius = self._centroid_and_radius(row)
94 | tsandN, times = self.transient_input(row, grouped, "rate")
95 | return {
96 | "xc": xc,
97 | "yc": yc,
98 | "R": radius,
99 | "tsandN": tsandN,
100 | "label": row["label"],
101 | }, times
102 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/head_line_sink.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
3 | from qgistim.core.elements.colors import BLUE
4 | from qgistim.core.elements.element import TransientElement
5 | from qgistim.core.elements.schemata import RowWiseSchema
6 | from qgistim.core.schemata import (
7 | AllOrNone,
8 | AllRequired,
9 | Membership,
10 | NotBoth,
11 | Optional,
12 | Positive,
13 | Required,
14 | StrictlyIncreasing,
15 | StrictlyPositive,
16 | )
17 |
18 |
19 | class HeadLineSinkSchema(RowWiseSchema):
20 | timml_schemata = {
21 | "geometry": Required(),
22 | "head": Required(),
23 | "resistance": Required(Positive()),
24 | "width": Required(StrictlyPositive()),
25 | "order": Required(Positive()),
26 | "layer": Required(Membership("aquifer layers")),
27 | }
28 | ttim_consistency_schemata = (
29 | AllOrNone("time_start", "time_end", "head_transient"),
30 | NotBoth("time_start", "timeseries_id"),
31 | )
32 | ttim_schemata = {
33 | "time_start": Optional(Positive()),
34 | "time_end": Optional(Positive()),
35 | "timeseries_id": Optional(Membership("ttim timeseries IDs")),
36 | }
37 | timeseries_schemata = {
38 | "timeseries_id": AllRequired(),
39 | "time_start": AllRequired(Positive(), StrictlyIncreasing()),
40 | "head": AllRequired(),
41 | }
42 |
43 |
44 | class HeadLineSink(TransientElement):
45 | element_type = "Head Line Sink"
46 | geometry_type = "Linestring"
47 | timml_attributes = (
48 | QgsField("head", QVariant.Double),
49 | QgsField("resistance", QVariant.Double),
50 | QgsField("width", QVariant.Double),
51 | QgsField("order", QVariant.Int),
52 | QgsField("layer", QVariant.Int),
53 | QgsField("label", QVariant.String),
54 | QgsField("time_start", QVariant.Double),
55 | QgsField("time_end", QVariant.Double),
56 | QgsField("head_transient", QVariant.Double),
57 | QgsField("timeseries_id", QVariant.Int),
58 | )
59 | ttim_attributes = (
60 | QgsField("timeseries_id", QVariant.Int),
61 | QgsField("time_start", QVariant.Double),
62 | QgsField("head", QVariant.Double),
63 | )
64 | timml_defaults = {
65 | "order": QgsDefaultValue("4"),
66 | }
67 | transient_columns = (
68 | "time_start",
69 | "time_end",
70 | "head_transient",
71 | "timeseries_id",
72 | )
73 | schema = HeadLineSinkSchema()
74 |
75 | @classmethod
76 | def renderer(cls) -> QgsSingleSymbolRenderer:
77 | return cls.line_renderer(color=BLUE, width="0.75")
78 |
79 | def process_timml_row(self, row, other=None):
80 | return {
81 | "xy": self.linestring_xy(row),
82 | "hls": row["head"],
83 | "res": row["resistance"],
84 | "wh": row["width"],
85 | "order": row["order"],
86 | "layers": row["layer"],
87 | "label": row["label"],
88 | }
89 |
90 | def process_ttim_row(self, row, grouped):
91 | tsandh, times = self.transient_input(row, grouped, "head")
92 | return {
93 | "xy": self.linestring_xy(row),
94 | "tsandh": tsandh,
95 | "res": row["resistance"],
96 | "wh": row["width"],
97 | "layers": row["layer"],
98 | "label": row["label"],
99 | }, times
100 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/line_sink_ditch.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
3 | from qgistim.core.elements.colors import GREEN
4 | from qgistim.core.elements.element import TransientElement
5 | from qgistim.core.elements.schemata import RowWiseSchema
6 | from qgistim.core.schemata import (
7 | AllOrNone,
8 | AllRequired,
9 | Membership,
10 | NotBoth,
11 | Optional,
12 | Positive,
13 | Required,
14 | StrictlyIncreasing,
15 | StrictlyPositive,
16 | )
17 |
18 |
19 | class LineSinkDitchSchema(RowWiseSchema):
20 | timml_schemata = {
21 | "geometry": Required(),
22 | "discharge": Required(),
23 | "resistance": Required(Positive()),
24 | "width": Required(StrictlyPositive()),
25 | "order": Required(Positive()),
26 | "layer": Required(Membership("aquifer layers")),
27 | }
28 | ttim_consistency_schemata = (
29 | AllOrNone("time_start", "time_end", "discharge_transient"),
30 | NotBoth("time_start", "timeseries_id"),
31 | )
32 | ttim_schemata = {
33 | "time_start": Optional(Positive()),
34 | "time_end": Optional(Positive()),
35 | "timeseries_id": Optional(Membership("ttim timeseries IDs")),
36 | }
37 | timeseries_schemata = {
38 | "timeseries_id": AllRequired(),
39 | "time_start": AllRequired(Positive(), StrictlyIncreasing()),
40 | "discharge": AllRequired(),
41 | }
42 |
43 |
44 | class LineSinkDitch(TransientElement):
45 | element_type = "Line Sink Ditch"
46 | geometry_type = "Linestring"
47 | timml_attributes = (
48 | QgsField("discharge", QVariant.Double),
49 | QgsField("resistance", QVariant.Double),
50 | QgsField("width", QVariant.Double),
51 | QgsField("order", QVariant.Int),
52 | QgsField("layer", QVariant.Int),
53 | QgsField("label", QVariant.String),
54 | QgsField("time_start", QVariant.Double),
55 | QgsField("time_end", QVariant.Double),
56 | QgsField("discharge_transient", QVariant.Double),
57 | QgsField("timeseries_id", QVariant.Int),
58 | )
59 | ttim_attributes = (
60 | QgsField("timeseries_id", QVariant.Int),
61 | QgsField("time_start", QVariant.Double),
62 | QgsField("discharge", QVariant.Double),
63 | )
64 | timml_defaults = {
65 | "order": QgsDefaultValue("4"),
66 | }
67 | transient_columns = (
68 | "time_start",
69 | "time_end",
70 | "discharge_transient",
71 | "timeseries_id",
72 | )
73 | schema = LineSinkDitchSchema()
74 |
75 | @classmethod
76 | def renderer(cls) -> QgsSingleSymbolRenderer:
77 | return cls.line_renderer(color=GREEN, width="0.75")
78 |
79 | def process_timml_row(self, row, other=None):
80 | return {
81 | "xy": self.linestring_xy(row),
82 | "Qls": row["discharge"],
83 | "res": row["resistance"],
84 | "wh": row["width"],
85 | "order": row["order"],
86 | "layers": row["layer"],
87 | "label": row["label"],
88 | }
89 |
90 | def process_ttim_row(self, row, grouped):
91 | tsandQ, times = self.transient_input(row, grouped, "discharge")
92 | return {
93 | "xy": self.linestring_xy(row),
94 | "tsandQ": tsandQ,
95 | "res": row["resistance"],
96 | "wh": row["width"],
97 | "layers": row["layer"],
98 | "label": row["label"],
99 | }, times
100 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/polygon_inhomogeneity.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
5 | from qgistim.core.elements.colors import GREY, TRANSPARENT_GREY
6 | from qgistim.core.elements.element import AssociatedElement
7 | from qgistim.core.elements.schemata import RowWiseSchema, TableSchema
8 | from qgistim.core.schemata import (
9 | AllGreaterEqual,
10 | AllRequired,
11 | Equals,
12 | Membership,
13 | OffsetAllRequired,
14 | OptionalFirstOnly,
15 | Positive,
16 | Required,
17 | SemiConfined,
18 | StrictlyDecreasing,
19 | StrictlyPositive,
20 | )
21 |
22 |
23 | class PolygonInhomogeneitySchema(RowWiseSchema):
24 | timml_schemata = {
25 | "geometry": Required(),
26 | "inhomogeneity_id": Required(Membership("properties inhomogeneity_id")),
27 | "order": Required(Positive()),
28 | "ndegrees": Required(Positive()),
29 | }
30 |
31 |
32 | class AssociatedPolygonInhomogeneitySchema(TableSchema):
33 | timml_schemata = {
34 | "inhomogeneity_id": AllRequired(),
35 | "layer": AllRequired(Equals("aquifer layers")),
36 | "aquifer_top": AllRequired(StrictlyDecreasing()),
37 | "aquifer_bottom": AllRequired(StrictlyDecreasing()),
38 | "aquitard_c": OffsetAllRequired(StrictlyPositive()),
39 | "aquifer_k": AllRequired(StrictlyPositive()),
40 | "semiconf_top": OptionalFirstOnly(),
41 | "semiconf_head": OptionalFirstOnly(),
42 | "rate": OptionalFirstOnly(),
43 | }
44 | timml_consistency_schemata = (
45 | SemiConfined(),
46 | AllGreaterEqual("aquifer_top", "aquifer_bottom"),
47 | )
48 |
49 |
50 | class PolygonInhomogeneity(AssociatedElement):
51 | element_type = "Polygon Inhomogeneity"
52 | geometry_type = "Polygon"
53 | timml_attributes = (
54 | QgsField("inhomogeneity_id", QVariant.Int),
55 | QgsField("order", QVariant.Int),
56 | QgsField("ndegrees", QVariant.Int),
57 | )
58 | assoc_attributes = [
59 | QgsField("inhomogeneity_id", QVariant.Int),
60 | QgsField("layer", QVariant.Int),
61 | QgsField("aquifer_top", QVariant.Double),
62 | QgsField("aquifer_bottom", QVariant.Double),
63 | QgsField("aquitard_c", QVariant.Double),
64 | QgsField("aquifer_k", QVariant.Double),
65 | QgsField("semiconf_top", QVariant.Double),
66 | QgsField("semiconf_head", QVariant.Double),
67 | QgsField("rate", QVariant.Double),
68 | QgsField("aquitard_npor", QVariant.Double),
69 | QgsField("aquifer_npor", QVariant.Double),
70 | ]
71 | timml_defaults = {
72 | "order": QgsDefaultValue("4"),
73 | "ndegrees": QgsDefaultValue("6"),
74 | "inhomogeneity_id": QgsDefaultValue("1"),
75 | }
76 | assoc_defaults = {
77 | "inhomogeneity_id": QgsDefaultValue("1"),
78 | }
79 | schema = PolygonInhomogeneitySchema()
80 | assoc_schema = AssociatedPolygonInhomogeneitySchema()
81 |
82 | @classmethod
83 | def renderer(cls) -> QgsSingleSymbolRenderer:
84 | return cls.polygon_renderer(
85 | color=TRANSPARENT_GREY, color_border=GREY, width_border="0.75"
86 | )
87 |
88 | def process_timml_row(self, row: Dict[str, Any], grouped: Dict[int, Any]):
89 | inhom_id = row["inhomogeneity_id"]
90 | raw_data = grouped[inhom_id]
91 | aquifer_data = self.aquifer_data(raw_data, transient=False)
92 | return {
93 | "xy": self.polygon_xy(row),
94 | "order": row["order"],
95 | "ndeg": row["ndegrees"],
96 | **aquifer_data,
97 | }
98 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/domain.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Tuple
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import (
5 | QgsFeature,
6 | QgsField,
7 | QgsGeometry,
8 | QgsPointXY,
9 | QgsSingleSymbolRenderer,
10 | )
11 | from qgistim.core.elements.colors import BLACK
12 | from qgistim.core.elements.element import ElementExtraction, TransientElement
13 | from qgistim.core.elements.schemata import SingleRowSchema
14 | from qgistim.core.schemata import AllRequired, Positive, Required, StrictlyIncreasing
15 |
16 |
17 | class DomainSchema(SingleRowSchema):
18 | timml_schemata = {"geometry": Required()}
19 | timeseries_schemata = {
20 | "time": AllRequired(Positive(), StrictlyIncreasing()),
21 | }
22 |
23 |
24 | class Domain(TransientElement):
25 | element_type = "Domain"
26 | geometry_type = "Polygon"
27 | ttim_attributes = (QgsField("time", QVariant.Double),)
28 | schema = DomainSchema()
29 |
30 | def __init__(self, path: str, name: str):
31 | self._initialize_default(path, name)
32 | self.timml_name = f"timml {self.element_type}:Domain"
33 | self.ttim_name = "ttim Computation Times:Domain"
34 |
35 | @classmethod
36 | def renderer(cls) -> QgsSingleSymbolRenderer:
37 | """
38 | Results in transparent fill, with a medium thick black border line.
39 | """
40 | return cls.polygon_renderer(
41 | color="255,0,0,0", color_border=BLACK, width_border="0.75"
42 | )
43 |
44 | def remove_from_geopackage(self):
45 | pass
46 |
47 | def update_extent(self, iface: Any) -> Tuple[float, float]:
48 | provider = self.timml_layer.dataProvider()
49 | provider.truncate() # removes all features
50 | canvas = iface.mapCanvas()
51 | extent = canvas.extent()
52 | xmin = extent.xMinimum()
53 | ymin = extent.yMinimum()
54 | xmax = extent.xMaximum()
55 | ymax = extent.yMaximum()
56 | points = [
57 | QgsPointXY(xmin, ymax),
58 | QgsPointXY(xmax, ymax),
59 | QgsPointXY(xmax, ymin),
60 | QgsPointXY(xmin, ymin),
61 | ]
62 | feature = QgsFeature()
63 | feature.setGeometry(QgsGeometry.fromPolygonXY([points]))
64 | provider.addFeatures([feature])
65 | canvas.refresh()
66 | return ymax, ymin
67 |
68 | def to_timml(self, other) -> ElementExtraction:
69 | data = self.table_to_records(layer=self.timml_layer)
70 | errors = self.schema.validate_timml(
71 | name=self.timml_layer.name(), data=data, other=other
72 | )
73 | if errors:
74 | return ElementExtraction(errors=errors)
75 | else:
76 | x = [point[0] for point in data[0]["geometry"]]
77 | y = [point[1] for point in data[0]["geometry"]]
78 | return ElementExtraction(
79 | data={
80 | "xmin": min(x),
81 | "xmax": max(x),
82 | "ymin": min(y),
83 | "ymax": max(y),
84 | }
85 | )
86 |
87 | def to_ttim(self, other) -> ElementExtraction:
88 | timml_extraction = self.to_timml(other)
89 | data = timml_extraction.data
90 |
91 | timeseries = self.table_to_dict(layer=self.ttim_layer)
92 | errors = self.schema.validate_timeseries(
93 | name=self.ttim_layer.name(), data=timeseries
94 | )
95 | if errors:
96 | return ElementExtraction(errors=errors)
97 | if timeseries["time"]:
98 | data["time"] = timeseries["time"]
99 | times = set(timeseries["time"])
100 | else:
101 | times = set()
102 | return ElementExtraction(data=data, times=times)
103 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/building_pit.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
5 | from qgistim.core.elements.colors import RED, TRANSPARENT_RED
6 | from qgistim.core.elements.element import AssociatedElement
7 | from qgistim.core.elements.schemata import RowWiseSchema, TableSchema
8 | from qgistim.core.schemata import (
9 | AllGreaterEqual,
10 | AllRequired,
11 | AtleastOneTrue,
12 | Equals,
13 | Membership,
14 | OffsetAllRequired,
15 | OptionalFirstOnly,
16 | Positive,
17 | Required,
18 | SemiConfined,
19 | StrictlyDecreasing,
20 | StrictlyPositive,
21 | )
22 |
23 |
24 | class BuildingPitSchema(RowWiseSchema):
25 | timml_schemata = {
26 | "geometry": Required(),
27 | "inhomogeneity_id": Required(Membership("properties inhomogeneity_id")),
28 | "order": Required(Positive()),
29 | "ndegrees": Required(Positive()),
30 | }
31 |
32 |
33 | class AssociatedBuildingPitSchema(TableSchema):
34 | timml_schemata = {
35 | "inhomogeneity_id": AllRequired(),
36 | "layer": AllRequired(Equals("aquifer layers")),
37 | "aquifer_top": AllRequired(StrictlyDecreasing()),
38 | "aquifer_bottom": AllRequired(StrictlyDecreasing()),
39 | "aquitard_c": OffsetAllRequired(StrictlyPositive()),
40 | "aquifer_k": AllRequired(StrictlyPositive()),
41 | "semiconf_top": OptionalFirstOnly(),
42 | "semiconf_head": OptionalFirstOnly(),
43 | "wall_in_layer": AllRequired(AtleastOneTrue()),
44 | }
45 | timml_consistency_schemata = (
46 | SemiConfined(),
47 | AllGreaterEqual("aquifer_top", "aquifer_bottom"),
48 | )
49 |
50 |
51 | class BuildingPit(AssociatedElement):
52 | element_type = "Building Pit"
53 | geometry_type = "Polygon"
54 | timml_attributes = (
55 | QgsField("inhomogeneity_id", QVariant.Int),
56 | QgsField("order", QVariant.Int),
57 | QgsField("ndegrees", QVariant.Int),
58 | )
59 | assoc_attributes = [
60 | QgsField("inhomogeneity_id", QVariant.Int),
61 | QgsField("layer", QVariant.Int),
62 | QgsField("aquifer_top", QVariant.Double),
63 | QgsField("aquifer_bottom", QVariant.Double),
64 | QgsField("aquitard_c", QVariant.Double),
65 | QgsField("aquifer_k", QVariant.Double),
66 | QgsField("semiconf_top", QVariant.Double),
67 | QgsField("semiconf_head", QVariant.Double),
68 | QgsField("wall_in_layer", QVariant.Bool),
69 | QgsField("aquitard_npor", QVariant.Double),
70 | QgsField("aquifer_npor", QVariant.Double),
71 | ]
72 | timml_defaults = {
73 | "order": QgsDefaultValue("4"),
74 | "ndegrees": QgsDefaultValue("6"),
75 | "inhomogeneity_id": QgsDefaultValue("1"),
76 | }
77 | assoc_defaults = {
78 | "inhomogeneity_id": QgsDefaultValue("1"),
79 | "wall_in_layer": QgsDefaultValue("false"),
80 | }
81 | schema = BuildingPitSchema()
82 | assoc_schema = AssociatedBuildingPitSchema()
83 |
84 | @classmethod
85 | def renderer(cls) -> QgsSingleSymbolRenderer:
86 | return cls.polygon_renderer(
87 | color=TRANSPARENT_RED, color_border=RED, width_border="0.75"
88 | )
89 |
90 | def process_timml_row(self, row: Dict[str, Any], grouped: Dict[int, Any]):
91 | inhom_id = row["inhomogeneity_id"]
92 | raw_data = grouped[inhom_id]
93 | layers = [i for i, active in enumerate(raw_data["wall_in_layer"]) if active]
94 | aquifer_data = self.aquifer_data(raw_data, transient=False)
95 | return {
96 | "xy": self.polygon_xy(row),
97 | "order": row["order"],
98 | "ndeg": row["ndegrees"],
99 | "layers": layers,
100 | **aquifer_data,
101 | }
102 |
--------------------------------------------------------------------------------
/docs/_static/QGIS-Tim-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
103 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/__init__.py:
--------------------------------------------------------------------------------
1 | import re
2 | from collections import defaultdict
3 | from functools import partial
4 | from typing import List, Tuple
5 |
6 | from qgistim.core import geopackage
7 | from qgistim.core.elements.aquifer import Aquifer
8 | from qgistim.core.elements.building_pit import BuildingPit
9 | from qgistim.core.elements.circular_area_sink import CircularAreaSink
10 | from qgistim.core.elements.constant import Constant
11 | from qgistim.core.elements.domain import Domain
12 | from qgistim.core.elements.discharge_observation import DischargeObservation
13 | from qgistim.core.elements.element import Element
14 | from qgistim.core.elements.head_line_sink import HeadLineSink
15 | from qgistim.core.elements.headwell import HeadWell, RemoteHeadWell
16 | from qgistim.core.elements.impermeable_line_doublet import ImpermeableLineDoublet
17 | from qgistim.core.elements.leaky_building_pit import LeakyBuildingPit
18 | from qgistim.core.elements.leaky_line_doublet import LeakyLineDoublet
19 | from qgistim.core.elements.line_sink_ditch import LineSinkDitch
20 | from qgistim.core.elements.observation import HeadObservation
21 | from qgistim.core.elements.polygon_area_sink import PolygonAreaSink
22 | from qgistim.core.elements.polygon_inhomogeneity import PolygonInhomogeneity
23 | from qgistim.core.elements.polygon_semi_confined_top import PolygonSemiConfinedTop
24 | from qgistim.core.elements.uniform_flow import UniformFlow
25 | from qgistim.core.elements.well import Well
26 |
27 | ELEMENTS = {
28 | element.element_type: element
29 | for element in (
30 | Aquifer,
31 | Domain,
32 | Constant,
33 | UniformFlow,
34 | Well,
35 | HeadWell,
36 | RemoteHeadWell,
37 | HeadLineSink,
38 | LineSinkDitch,
39 | CircularAreaSink,
40 | ImpermeableLineDoublet,
41 | LeakyLineDoublet,
42 | PolygonAreaSink,
43 | PolygonSemiConfinedTop,
44 | PolygonInhomogeneity,
45 | BuildingPit,
46 | LeakyBuildingPit,
47 | HeadObservation,
48 | DischargeObservation,
49 | )
50 | }
51 |
52 |
53 | def parse_name(layername: str) -> Tuple[str, str, str]:
54 | """
55 | Based on the layer name find out:
56 |
57 | * whether it's a timml or ttim element;
58 | * which element type it is;
59 | * what the user provided name is.
60 |
61 | For example:
62 | parse_name("timml Headwell:drainage") -> ("timml", "Head Well", "drainage")
63 | """
64 | prefix, name = layername.split(":")
65 | element_type = re.split("timml |ttim ", prefix)[1]
66 | mapping = {
67 | "Computation Times": "Domain",
68 | "Temporal Settings": "Aquifer",
69 | "Polygon Inhomogeneity Properties": "Polygon Inhomogeneity",
70 | "Building Pit Properties": "Building Pit",
71 | "Leaky Building Pit Properties": "Leaky Building Pit",
72 | }
73 | element_type = mapping.get(element_type, element_type)
74 | if "timml" in prefix:
75 | if "Properties" in prefix:
76 | tim_type = "timml_assoc"
77 | else:
78 | tim_type = "timml"
79 | elif "ttim" in prefix:
80 | tim_type = "ttim"
81 | else:
82 | raise ValueError("Neither timml nor ttim in layername")
83 | return tim_type, element_type, name
84 |
85 |
86 | def load_elements_from_geopackage(path: str) -> List[Element]:
87 | # List the names in the geopackage
88 | gpkg_names = geopackage.layers(path)
89 |
90 | # Group them on the basis of name
91 | dd = defaultdict
92 | grouped_names = dd(partial(dd, partial(dd, list)))
93 | for layername in gpkg_names:
94 | tim_type, element_type, name = parse_name(layername)
95 | grouped_names[element_type][name][tim_type] = layername
96 |
97 | elements = []
98 | for element_type, group in grouped_names.items():
99 | for name in group:
100 | elements.append(ELEMENTS[element_type](path, name))
101 |
102 | return elements
103 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/well.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
5 | from qgistim.core.elements.colors import GREEN
6 | from qgistim.core.elements.element import TransientElement
7 | from qgistim.core.elements.schemata import RowWiseSchema
8 | from qgistim.core.schemata import (
9 | AllOrNone,
10 | AllRequired,
11 | ConditionallyRequired,
12 | Membership,
13 | NotBoth,
14 | Optional,
15 | Positive,
16 | Required,
17 | StrictlyPositive,
18 | )
19 |
20 |
21 | class WellSchema(RowWiseSchema):
22 | timml_schemata = {
23 | "geometry": Required(),
24 | "discharge": Required(),
25 | "radius": Required(StrictlyPositive()),
26 | "resistance": Required(Positive()),
27 | "layer": Required(Membership("aquifer layers")),
28 | }
29 | timml_consistency_schemata = (ConditionallyRequired("slug", "caisson_radius"),)
30 | ttim_schemata = {
31 | "caisson_radius": Optional(StrictlyPositive()),
32 | "slug": Required(),
33 | "time_start": Optional(Positive()),
34 | "time_end": Optional(Positive()),
35 | "timeseries_id": Optional(Membership("ttim timeseries IDs")),
36 | }
37 | ttim_consistency_schemata = (
38 | AllOrNone(("time_start", "time_end", "discharge_transient")),
39 | NotBoth("time_start", "timeseries_id"),
40 | )
41 | timeseries_schemata = {
42 | "timeseries_id": AllRequired(),
43 | "time_start": AllRequired(Positive()),
44 | "discharge": AllRequired(),
45 | }
46 |
47 |
48 | class Well(TransientElement):
49 | element_type = "Well"
50 | geometry_type = "Point"
51 | timml_attributes = (
52 | QgsField("discharge", QVariant.Double),
53 | QgsField("radius", QVariant.Double),
54 | QgsField("resistance", QVariant.Double),
55 | QgsField("layer", QVariant.Int),
56 | QgsField("label", QVariant.String),
57 | QgsField("time_start", QVariant.Double),
58 | QgsField("time_end", QVariant.Double),
59 | QgsField("discharge_transient", QVariant.Double),
60 | QgsField("caisson_radius", QVariant.Double),
61 | QgsField("slug", QVariant.Bool),
62 | QgsField("timeseries_id", QVariant.Int),
63 | )
64 | ttim_attributes = (
65 | QgsField("timeseries_id", QVariant.Int),
66 | QgsField("time_start", QVariant.Double),
67 | QgsField("discharge", QVariant.Double),
68 | )
69 | timml_defaults = {
70 | "radius": QgsDefaultValue("0.1"),
71 | "resistance": QgsDefaultValue("0.0"),
72 | "slug": QgsDefaultValue("False"),
73 | }
74 | transient_columns = (
75 | "time_start",
76 | "time_end",
77 | "discharge_transient",
78 | "caisson_radius",
79 | "slug",
80 | "timeseries_id",
81 | )
82 | schema = WellSchema()
83 |
84 | @classmethod
85 | def renderer(cls) -> QgsSingleSymbolRenderer:
86 | return cls.marker_renderer(color=GREEN, size="3")
87 |
88 | def process_timml_row(self, row, other=None) -> Dict[str, Any]:
89 | x, y = self.point_xy(row)
90 | return {
91 | "xw": x,
92 | "yw": y,
93 | "Qw": row["discharge"],
94 | "rw": row["radius"],
95 | "res": row["resistance"],
96 | "layers": row["layer"],
97 | "label": row["label"],
98 | }
99 |
100 | def process_ttim_row(self, row, grouped):
101 | x, y = self.point_xy(row)
102 | tsandQ, times = self.transient_input(row, grouped, "discharge")
103 | return {
104 | "xw": x,
105 | "yw": y,
106 | "tsandQ": tsandQ,
107 | "rw": row["radius"],
108 | "res": row["resistance"],
109 | "layers": row["layer"],
110 | "label": row["label"],
111 | "rc": row["caisson_radius"],
112 | "wbstype": "slug" if row["slug"] else "pumping",
113 | }, times
114 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/layer_styling.py:
--------------------------------------------------------------------------------
1 | """
2 | Some layer styling for output.
3 |
4 | We'd like contours to look the same consistently. A Domain should be
5 | transparent, not obscuring the view. A head grid should have pseudocoloring,
6 | ideally with a legend stretching from minimum to maximum.
7 | """
8 | from typing import List
9 |
10 | from PyQt5.QtGui import QColor
11 | from qgis.core import (
12 | QgsColorRampShader,
13 | QgsLineSymbol,
14 | QgsPalLayerSettings,
15 | QgsRasterBandStats,
16 | QgsRasterShader,
17 | QgsSingleBandPseudoColorRenderer,
18 | QgsSingleSymbolRenderer,
19 | QgsStyle,
20 | QgsTextBufferSettings,
21 | QgsTextFormat,
22 | QgsVectorLayerSimpleLabeling,
23 | )
24 |
25 |
26 | def color_ramp_items(
27 | colormap: str, minimum: float, maximum: float, nclass: int
28 | ) -> List[QgsColorRampShader.ColorRampItem]:
29 | """
30 | Parameters
31 | ----------
32 | colormap: str
33 | Name of QGIS colormap
34 | minimum: float
35 | maximum: float
36 | nclass: int
37 | Number of colormap classes to create
38 |
39 | Returns
40 | -------
41 | color_ramp_items: List[QgsColorRampShader.ColorRampItem]
42 | Can be used directly by the QgsColorRampShader
43 | """
44 | delta = maximum - minimum
45 | fractional_steps = [i / nclass for i in range(nclass + 1)]
46 | ramp = QgsStyle().defaultStyle().colorRamp(colormap)
47 | colors = [ramp.color(f) for f in fractional_steps]
48 | steps = [minimum + f * delta for f in fractional_steps]
49 | return ramp, [
50 | QgsColorRampShader.ColorRampItem(step, color, str(step))
51 | for step, color in zip(steps, colors)
52 | ]
53 |
54 |
55 | def pseudocolor_renderer(
56 | layer, band: int, colormap: str, nclass: int
57 | ) -> QgsSingleBandPseudoColorRenderer:
58 | """
59 | Parameters
60 | ----------
61 | layer: QGIS map layer
62 | band: int
63 | band number of the raster to create a renderer for
64 | colormap: str
65 | Name of QGIS colormap
66 | nclass: int
67 | Number of colormap classes to create
68 |
69 | Returns
70 | -------
71 | renderer: QgsSingleBandPseudoColorRenderer
72 | """
73 | stats = layer.dataProvider().bandStatistics(band, QgsRasterBandStats.All)
74 | minimum = stats.minimumValue
75 | maximum = stats.maximumValue
76 |
77 | ramp, ramp_items = color_ramp_items(colormap, minimum, maximum, nclass)
78 | shader_function = QgsColorRampShader()
79 | shader_function.setMinimumValue(minimum)
80 | shader_function.setMaximumValue(maximum)
81 | shader_function.setSourceColorRamp(ramp)
82 | shader_function.setColorRampType(QgsColorRampShader.Interpolated)
83 | shader_function.setClassificationMode(QgsColorRampShader.EqualInterval)
84 | shader_function.setColorRampItemList(ramp_items)
85 |
86 | raster_shader = QgsRasterShader()
87 | raster_shader.setRasterShaderFunction(shader_function)
88 |
89 | return QgsSingleBandPseudoColorRenderer(layer.dataProvider(), band, raster_shader)
90 |
91 |
92 | def contour_renderer() -> QgsSingleSymbolRenderer:
93 | symbol = QgsLineSymbol.createSimple(
94 | {
95 | "color": "#000000#", # black
96 | "width": "0.25",
97 | }
98 | )
99 | return QgsSingleSymbolRenderer(symbol)
100 |
101 |
102 | def number_labels(field: str) -> QgsVectorLayerSimpleLabeling:
103 | pal_layer = QgsPalLayerSettings()
104 | pal_layer.fieldName = field
105 | pal_layer.enabled = True
106 | pal_layer.placement = QgsPalLayerSettings.Line
107 | pal_layer.formatNumbers = True
108 | pal_layer.decimals = 2
109 |
110 | buffer_settings = QgsTextBufferSettings()
111 | buffer_settings.setEnabled(True)
112 | buffer_settings.setSize(1)
113 | buffer_settings.setColor(QColor("white"))
114 |
115 | text_format = QgsTextFormat()
116 | text_format.setBuffer(buffer_settings)
117 |
118 | pal_layer.setFormat(text_format)
119 | labels = QgsVectorLayerSimpleLabeling(pal_layer)
120 | return labels
121 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/leaky_building_pit.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from PyQt5.QtCore import QVariant
4 | from qgis.core import QgsDefaultValue, QgsField, QgsSingleSymbolRenderer
5 | from qgistim.core.elements.colors import RED, TRANSPARENT_RED
6 | from qgistim.core.elements.element import AssociatedElement
7 | from qgistim.core.elements.schemata import RowWiseSchema, TableSchema
8 | from qgistim.core.schemata import (
9 | AllGreaterEqual,
10 | AllRequired,
11 | AtleastOneTrue,
12 | Equals,
13 | Membership,
14 | OffsetAllRequired,
15 | OptionalFirstOnly,
16 | Positive,
17 | Required,
18 | RequiredFirstOnly,
19 | SemiConfined,
20 | StrictlyDecreasing,
21 | StrictlyPositive,
22 | )
23 |
24 |
25 | class LeakyBuildingPitSchema(RowWiseSchema):
26 | timml_schemata = {
27 | "geometry": Required(),
28 | "inhomogeneity_id": Required(Membership("properties inhomogeneity_id")),
29 | "order": Required(Positive()),
30 | "ndegrees": Required(Positive()),
31 | }
32 |
33 |
34 | class AssociatedLeakyBuildingPitchema(TableSchema):
35 | timml_schemata = {
36 | "inhomogeneity_id": AllRequired(),
37 | "layer": AllRequired(Equals("aquifer layers")),
38 | "aquifer_top": AllRequired(StrictlyDecreasing()),
39 | "aquifer_bottom": AllRequired(StrictlyDecreasing()),
40 | "aquitard_c": OffsetAllRequired(StrictlyPositive()),
41 | "aquifer_k": AllRequired(StrictlyPositive()),
42 | "semiconf_top": OptionalFirstOnly(),
43 | "semiconf_head": OptionalFirstOnly(),
44 | "resistance": RequiredFirstOnly(StrictlyPositive()),
45 | "wall_in_layer": AllRequired(AtleastOneTrue()),
46 | }
47 | timml_consistency_schemata = (
48 | SemiConfined(),
49 | AllGreaterEqual("aquifer_top", "aquifer_bottom"),
50 | )
51 |
52 |
53 | class LeakyBuildingPit(AssociatedElement):
54 | element_type = "Leaky Building Pit"
55 | geometry_type = "Polygon"
56 | timml_attributes = (
57 | QgsField("inhomogeneity_id", QVariant.Int),
58 | QgsField("order", QVariant.Int),
59 | QgsField("ndegrees", QVariant.Int),
60 | )
61 | assoc_attributes = [
62 | QgsField("inhomogeneity_id", QVariant.Int),
63 | QgsField("layer", QVariant.Int),
64 | QgsField("aquifer_top", QVariant.Double),
65 | QgsField("aquifer_bottom", QVariant.Double),
66 | QgsField("aquitard_c", QVariant.Double),
67 | QgsField("aquifer_k", QVariant.Double),
68 | QgsField("semiconf_top", QVariant.Double),
69 | QgsField("semiconf_head", QVariant.Double),
70 | QgsField("resistance", QVariant.Double),
71 | QgsField("wall_in_layer", QVariant.Bool),
72 | QgsField("aquitard_npor", QVariant.Double),
73 | QgsField("aquifer_npor", QVariant.Double),
74 | ]
75 | timml_defaults = {
76 | "order": QgsDefaultValue("4"),
77 | "ndegrees": QgsDefaultValue("6"),
78 | "inhomogeneity_id": QgsDefaultValue("1"),
79 | }
80 | assoc_defaults = {
81 | "inhomogeneity_id": QgsDefaultValue("1"),
82 | "wall_in_layer": QgsDefaultValue("false"),
83 | }
84 | schema = LeakyBuildingPitSchema()
85 | assoc_schema = AssociatedLeakyBuildingPitchema()
86 |
87 | @classmethod
88 | def renderer(cls) -> QgsSingleSymbolRenderer:
89 | return cls.polygon_renderer(
90 | color=TRANSPARENT_RED,
91 | color_border=RED,
92 | width_border="0.75",
93 | outline_style="dash",
94 | )
95 |
96 | def process_timml_row(self, row: Dict[str, Any], grouped: Dict[int, Any]):
97 | inhom_id = row["inhomogeneity_id"]
98 | raw_data = grouped[inhom_id]
99 | aquifer_data = self.aquifer_data(raw_data, transient=False)
100 | layers = [i for i, active in enumerate(raw_data["wall_in_layer"]) if active]
101 | return {
102 | "xy": self.polygon_xy(row),
103 | "order": row["order"],
104 | "ndeg": row["ndegrees"],
105 | "layers": layers,
106 | "res": raw_data["resistance"][0],
107 | **aquifer_data,
108 | }
109 |
--------------------------------------------------------------------------------
/gistim/geomet/util.py:
--------------------------------------------------------------------------------
1 | # Copyright 2013 Lars Butler & individual contributors
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | import collections.abc as collections
15 | import itertools
16 |
17 |
18 | def block_splitter(data, block_size):
19 | """
20 | Creates a generator by slicing ``data`` into chunks of ``block_size``.
21 |
22 | >>> data = range(10)
23 | >>> list(block_splitter(data, 2))
24 | [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]
25 |
26 | If ``data`` cannot be evenly divided by ``block_size``, the last block will
27 | simply be the remainder of the data. Example:
28 |
29 | >>> data = range(10)
30 | >>> list(block_splitter(data, 3))
31 | [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
32 |
33 | If the ``block_size`` is greater than the total length of ``data``, a
34 | single block will be generated:
35 |
36 | >>> data = range(3)
37 | >>> list(block_splitter(data, 4))
38 | [[0, 1, 2]]
39 |
40 | :param data:
41 | Any iterable. If ``data`` is a generator, it will be exhausted,
42 | obviously.
43 | :param int block_site:
44 | Desired (maximum) block size.
45 | """
46 | buf = []
47 | for i, datum in enumerate(data):
48 | buf.append(datum)
49 | if len(buf) == block_size:
50 | yield buf
51 | buf = []
52 |
53 | # If there's anything leftover (a partial block),
54 | # yield it as well.
55 | if buf:
56 | yield buf
57 |
58 |
59 | def take(n, iterable):
60 | """
61 | Return first n items of the iterable as a list
62 |
63 | Copied shamelessly from
64 | http://docs.python.org/2/library/itertools.html#recipes.
65 | """
66 | return list(itertools.islice(iterable, n))
67 |
68 |
69 | def as_bin_str(a_list):
70 | return bytes(a_list)
71 |
72 |
73 | def round_geom(geom, precision=None):
74 | """Round coordinates of a geometric object to given precision."""
75 | if geom["type"] == "Point":
76 | x, y = geom["coordinates"]
77 | xp, yp = [x], [y]
78 | if precision is not None:
79 | xp = [round(v, precision) for v in xp]
80 | yp = [round(v, precision) for v in yp]
81 | new_coords = tuple(zip(xp, yp))[0]
82 | if geom["type"] in ["LineString", "MultiPoint"]:
83 | xp, yp = zip(*geom["coordinates"])
84 | if precision is not None:
85 | xp = [round(v, precision) for v in xp]
86 | yp = [round(v, precision) for v in yp]
87 | new_coords = tuple(zip(xp, yp))
88 | elif geom["type"] in ["Polygon", "MultiLineString"]:
89 | new_coords = []
90 | for piece in geom["coordinates"]:
91 | xp, yp = zip(*piece)
92 | if precision is not None:
93 | xp = [round(v, precision) for v in xp]
94 | yp = [round(v, precision) for v in yp]
95 | new_coords.append(tuple(zip(xp, yp)))
96 | elif geom["type"] == "MultiPolygon":
97 | parts = geom["coordinates"]
98 | new_coords = []
99 | for part in parts:
100 | inner_coords = []
101 | for ring in part:
102 | xp, yp = zip(*ring)
103 | if precision is not None:
104 | xp = [round(v, precision) for v in xp]
105 | yp = [round(v, precision) for v in yp]
106 | inner_coords.append(tuple(zip(xp, yp)))
107 | new_coords.append(inner_coords)
108 | return {"type": geom["type"], "coordinates": new_coords}
109 |
110 |
111 | def flatten_multi_dim(sequence):
112 | """Flatten a multi-dimensional array-like to a single dimensional sequence
113 | (as a generator).
114 | """
115 | for x in sequence:
116 | if isinstance(x, collections.Iterable) and not isinstance(x, str):
117 | for y in flatten_multi_dim(x):
118 | yield y
119 | else:
120 | yield x
121 |
122 |
123 | def endian_token(is_little_endian):
124 | if is_little_endian:
125 | return "<"
126 | else:
127 | return ">"
128 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/headwell.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from PyQt5.QtCore import QVariant
4 | from PyQt5.QtGui import QColor
5 | from qgis.core import (
6 | QgsArrowSymbolLayer,
7 | QgsDefaultValue,
8 | QgsField,
9 | QgsLineSymbol,
10 | QgsSingleSymbolRenderer,
11 | )
12 | from qgistim.core.elements.colors import BLUE
13 | from qgistim.core.elements.element import TransientElement
14 | from qgistim.core.elements.schemata import RowWiseSchema
15 | from qgistim.core.schemata import (
16 | AllOrNone,
17 | AllRequired,
18 | Membership,
19 | NotBoth,
20 | Optional,
21 | Positive,
22 | Required,
23 | StrictlyIncreasing,
24 | StrictlyPositive,
25 | )
26 |
27 |
28 | class HeadWellSchema(RowWiseSchema):
29 | timml_schemata = {
30 | "geometry": Required(),
31 | "head": Required(),
32 | "radius": Required(StrictlyPositive()),
33 | "resistance": Required(Positive()),
34 | "layer": Required(Membership("aquifer layers")),
35 | }
36 | ttim_schemata = {
37 | "time_start": Optional(Positive()),
38 | "time_end": Optional(Positive()),
39 | "timeseries_id": Optional(Membership("ttim timeseries IDs")),
40 | }
41 | ttim_consistency_schemata = (
42 | AllOrNone(("time_start", "time_end", "head_transient")),
43 | NotBoth("time_start", "timeseries_id"),
44 | )
45 | timeseries_schemata = {
46 | "timeseries_id": AllRequired(),
47 | "time_start": AllRequired(Positive(), StrictlyIncreasing()),
48 | "head": AllRequired(),
49 | }
50 |
51 |
52 | class HeadWell(TransientElement):
53 | element_type = "Head Well"
54 | geometry_type = "Point"
55 | timml_attributes = (
56 | QgsField("head", QVariant.Double),
57 | QgsField("radius", QVariant.Double),
58 | QgsField("resistance", QVariant.Double),
59 | QgsField("layer", QVariant.Int),
60 | QgsField("label", QVariant.String),
61 | QgsField("time_start", QVariant.Double),
62 | QgsField("time_end", QVariant.Double),
63 | QgsField("head_transient", QVariant.Double),
64 | QgsField("timeseries_id", QVariant.Int),
65 | )
66 | ttim_attributes = (
67 | QgsField("timeseries_id", QVariant.Int),
68 | QgsField("time_start", QVariant.Double),
69 | QgsField("head", QVariant.Double),
70 | )
71 | timml_defaults = {
72 | "radius": QgsDefaultValue("0.1"),
73 | "resistance": QgsDefaultValue("0.0"),
74 | }
75 | transient_columns = (
76 | "time_start",
77 | "time_end",
78 | "head_transient",
79 | "timeseries_id",
80 | )
81 | schema = HeadWellSchema()
82 |
83 | @classmethod
84 | def renderer(cls) -> QgsSingleSymbolRenderer:
85 | return cls.marker_renderer(color=BLUE, size="3")
86 |
87 | def process_timml_row(self, row, other=None) -> Dict[str, Any]:
88 | x, y = self.point_xy(row)
89 | return {
90 | "xw": x,
91 | "yw": y,
92 | "hw": row["head"],
93 | "rw": row["radius"],
94 | "res": row["resistance"],
95 | "layers": row["layer"],
96 | "label": row["label"],
97 | }
98 |
99 | def process_ttim_row(self, row, grouped):
100 | x, y = self.point_xy(row)
101 | tsandh, times = self.transient_input(row, grouped, "head")
102 | return {
103 | "xw": x,
104 | "yw": y,
105 | "tsandh": tsandh,
106 | "rw": row["radius"],
107 | "res": row["resistance"],
108 | "layers": row["layer"],
109 | "label": row["label"],
110 | }, times
111 |
112 |
113 | class RemoteHeadWell(HeadWell):
114 | element_type = "Remote Head Well"
115 | geometry_type = "Linestring"
116 |
117 | @classmethod
118 | def renderer(cls) -> QgsSingleSymbolRenderer:
119 | arrow = QgsArrowSymbolLayer()
120 | red, green, blue, _ = [int(v) for v in BLUE.split(",")]
121 | arrow.setColor(QColor(red, green, blue))
122 | arrow.setHeadLength(2.5)
123 | symbol = QgsLineSymbol.createSimple({})
124 | symbol.changeSymbolLayer(0, arrow)
125 | return QgsSingleSymbolRenderer(symbol)
126 |
127 | def process_timml_row(self, row, other=None) -> Dict[str, Any]:
128 | xy = self.linestring_xy(row)
129 | xw, yw = xy[-1]
130 | xc, yc = xy[0]
131 | return {
132 | "xw": xw,
133 | "yw": yw,
134 | "hw": row["head"],
135 | "rw": row["radius"],
136 | "res": row["resistance"],
137 | "layers": row["layer"],
138 | "label": row["label"],
139 | "xc": xc,
140 | "yc": yc,
141 | }
142 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/schemata.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from collections import defaultdict
3 | from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union
4 |
5 | from qgistim.core.schemata import (
6 | ConsistencySchema,
7 | IterableSchemaContainer,
8 | SchemaContainer,
9 | )
10 |
11 |
12 | class ValidationData(NamedTuple):
13 | schemata: Dict[str, SchemaContainer]
14 | consistency_schemata: Tuple[ConsistencySchema]
15 | name: str
16 | data: Dict[str, Any]
17 | other: Optional[Dict[str, Any]] = None
18 |
19 |
20 | class SchemaBase(abc.ABC):
21 | # TODO: check for presence of columns
22 | timml_schemata: Dict[str, Union[SchemaContainer, IterableSchemaContainer]] = {}
23 | timml_consistency_schemata: Tuple[ConsistencySchema] = ()
24 | ttim_schemata: Dict[str, Union[SchemaContainer, IterableSchemaContainer]] = {}
25 | ttim_consistency_schemata: Tuple[ConsistencySchema] = ()
26 | timeseries_schemata: Dict[str, Union[SchemaContainer, IterableSchemaContainer]] = {}
27 |
28 | @staticmethod
29 | def _validate_table(vd: ValidationData) -> Dict[str, List]:
30 | errors = defaultdict(list)
31 | for variable, schema in vd.schemata.items():
32 | _errors = schema.validate(vd.data[variable], vd.other)
33 | if _errors:
34 | errors[f"{vd.name} {variable}"].extend(_errors)
35 |
36 | # The consistency schema rely on the row input being valid.
37 | # Hence, they are tested second.
38 | if not errors:
39 | for schema in vd.consistency_schemata:
40 | _error = schema.validate(vd.data, vd.other)
41 | if _error:
42 | errors[vd.name].append(_error)
43 |
44 | return errors
45 |
46 | @classmethod
47 | def validate_timeseries(
48 | cls, name: str, data: Dict[str, Any], other=None
49 | ) -> Dict[str, List]:
50 | vd = ValidationData(cls.timeseries_schemata, (), name, data, other)
51 | return cls._validate_table(vd)
52 |
53 | @classmethod
54 | def validate_timml(
55 | cls, name: str, data: Dict[str, Any], other=None
56 | ) -> Dict[str, List]:
57 | vd = ValidationData(
58 | cls.timml_schemata, cls.timml_consistency_schemata, name, data, other
59 | )
60 | return cls._validate(vd)
61 |
62 | @classmethod
63 | def validate_ttim(
64 | cls, name: str, data: Dict[str, Any], other=None
65 | ) -> Dict[str, List]:
66 | vd = ValidationData(
67 | cls.ttim_schemata, cls.ttim_consistency_schemata, name, data, other
68 | )
69 | return cls._validate(vd)
70 |
71 | @abc.abstractclassmethod
72 | def _validate(vd: ValidationData) -> Dict[str, List]:
73 | pass
74 |
75 |
76 | class TableSchema(SchemaBase, abc.ABC):
77 | """
78 | Schema for Tabular data, such as Aquifer properties.
79 | """
80 |
81 | @classmethod
82 | def _validate(
83 | cls,
84 | vd: ValidationData,
85 | ) -> Dict[str, List]:
86 | return cls._validate_table(vd)
87 |
88 |
89 | class RowWiseSchema(SchemaBase, abc.ABC):
90 | """
91 | Schema for entries that should be validated row-by-row, such as Wells.
92 | """
93 |
94 | @staticmethod
95 | def _validate(vd: ValidationData) -> Dict[str, List]:
96 | errors = defaultdict(list)
97 |
98 | for i, row in enumerate(vd.data):
99 | row_errors = defaultdict(list)
100 |
101 | for variable, schema in vd.schemata.items():
102 | _errors = schema.validate(row[variable], vd.other)
103 | if _errors:
104 | row_errors[variable].extend(_errors)
105 |
106 | # Skip consistency tests if the individual values are not good.
107 | if not row_errors:
108 | for schema in vd.consistency_schemata:
109 | _error = schema.validate(row, vd.other)
110 | if _error:
111 | row_errors["Row:"].append(_error)
112 |
113 | if row_errors:
114 | errors[f"Row {i + 1}:"] = row_errors
115 |
116 | return errors
117 |
118 |
119 | class SingleRowSchema(RowWiseSchema, abc.ABC):
120 | """
121 | Schema for entries that should contain only one row, which should be
122 | validated as a row, such as Constant, Domain, Uniform Flow.
123 | """
124 |
125 | @staticmethod
126 | def _validate(vd: ValidationData) -> Dict[str, List]:
127 | nrow = len(vd.data)
128 | if nrow != 1:
129 | return {
130 | vd.name: [
131 | f"Table must contain a single row. Table contains {nrow} rows."
132 | ]
133 | }
134 | return RowWiseSchema._validate(vd)
135 |
--------------------------------------------------------------------------------
/plugin/qgistim/core/elements/aquifer.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QVariant
2 | from qgis.core import QgsDefaultValue, QgsField
3 | from qgistim.core import geopackage
4 | from qgistim.core.elements.element import ElementExtraction, TransientElement
5 | from qgistim.core.elements.schemata import SingleRowSchema, TableSchema
6 | from qgistim.core.schemata import (
7 | AllGreaterEqual,
8 | AllRequired,
9 | OffsetAllRequired,
10 | OptionalFirstOnly,
11 | Positive,
12 | Range,
13 | Required,
14 | SemiConfined,
15 | StrictlyDecreasing,
16 | StrictlyPositive,
17 | )
18 |
19 |
20 | class AquiferSchema(TableSchema):
21 | timml_schemata = {
22 | "layer": AllRequired(Range()),
23 | "aquifer_top": AllRequired(StrictlyDecreasing()),
24 | "aquifer_bottom": AllRequired(StrictlyDecreasing()),
25 | "aquitard_c": OffsetAllRequired(StrictlyPositive()),
26 | "aquifer_k": AllRequired(StrictlyPositive()),
27 | "semiconf_top": OptionalFirstOnly(),
28 | "semiconf_head": OptionalFirstOnly(),
29 | }
30 | timml_consistency_schemata = (
31 | SemiConfined(),
32 | AllGreaterEqual("aquifer_top", "aquifer_bottom"),
33 | )
34 | ttim_schemata = {
35 | "aquitard_s": OffsetAllRequired(Positive()),
36 | "aquifer_s": AllRequired(Positive()),
37 | }
38 |
39 |
40 | class TemporalSettingsSchema(SingleRowSchema):
41 | ttim_schemata = {
42 | "time_min": Required(StrictlyPositive()),
43 | "laplace_inversion_M": Required(StrictlyPositive()),
44 | "start_date": Required(),
45 | }
46 |
47 |
48 | class Aquifer(TransientElement):
49 | element_type = "Aquifer"
50 | geometry_type = "No Geometry"
51 | timml_attributes = [
52 | QgsField("layer", QVariant.Int),
53 | QgsField("aquifer_top", QVariant.Double),
54 | QgsField("aquifer_bottom", QVariant.Double),
55 | QgsField("aquitard_c", QVariant.Double),
56 | QgsField("aquifer_k", QVariant.Double),
57 | QgsField("semiconf_top", QVariant.Double),
58 | QgsField("semiconf_head", QVariant.Double),
59 | QgsField("aquitard_s", QVariant.Double),
60 | QgsField("aquifer_s", QVariant.Double),
61 | QgsField("aquitard_npor", QVariant.Double),
62 | QgsField("aquifer_npor", QVariant.Double),
63 | ]
64 | ttim_attributes = (
65 | QgsField("time_min", QVariant.Double),
66 | QgsField("laplace_inversion_M", QVariant.Int),
67 | QgsField("start_date", QVariant.DateTime),
68 | )
69 | ttim_defaults = {
70 | "time_min": QgsDefaultValue("0.01"),
71 | "laplace_inversion_M": QgsDefaultValue("10"),
72 | }
73 | transient_columns = (
74 | "aquitard_s",
75 | "aquifer_s",
76 | "aquitard_npor",
77 | "aquifer_npor",
78 | )
79 | schema = AquiferSchema()
80 | assoc_schema = TemporalSettingsSchema()
81 |
82 | def __init__(self, path: str, name: str):
83 | self._initialize_default(path, name)
84 | self.timml_name = f"timml {self.element_type}:Aquifer"
85 | self.ttim_name = "ttim Temporal Settings:Aquifer"
86 |
87 | def write(self):
88 | self.timml_layer = geopackage.write_layer(
89 | self.path, self.timml_layer, self.timml_name, newfile=True
90 | )
91 | self.ttim_layer = geopackage.write_layer(
92 | self.path, self.ttim_layer, self.ttim_name
93 | )
94 | self.set_defaults()
95 |
96 | def remove_from_geopackage(self):
97 | """This element may not be removed."""
98 | return
99 |
100 | def to_timml(self) -> ElementExtraction:
101 | missing = self.check_timml_columns()
102 | if missing:
103 | return ElementExtraction(errors=missing)
104 |
105 | data = self.table_to_dict(layer=self.timml_layer)
106 | errors = self.schema.validate_timml(name=self.timml_layer.name(), data=data)
107 | return ElementExtraction(errors=errors, data=data)
108 |
109 | def to_ttim(self) -> ElementExtraction:
110 | missing = self.check_ttim_columns()
111 | if missing:
112 | return ElementExtraction(errors=missing)
113 |
114 | data = self.table_to_dict(layer=self.timml_layer)
115 | time_data = self.table_to_records(layer=self.ttim_layer)
116 | errors = {
117 | **self.schema.validate_ttim(name=self.timml_layer.name(), data=data),
118 | **self.assoc_schema.validate_ttim(
119 | name=self.ttim_layer.name(), data=time_data
120 | ),
121 | }
122 | if errors:
123 | return ElementExtraction(errors=errors)
124 | return ElementExtraction(data={**data, **time_data[0]})
125 |
126 | def extract_data(self, transient: bool) -> ElementExtraction:
127 | if transient:
128 | return self.to_ttim()
129 | else:
130 | return self.to_timml()
131 |
--------------------------------------------------------------------------------
/docs/tutorial-QGIS-TIM_files/libs/quarto-html/anchor.min.js:
--------------------------------------------------------------------------------
1 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
2 | //
3 | // AnchorJS - v4.3.1 - 2021-04-17
4 | // https://www.bryanbraun.com/anchorjs/
5 | // Copyright (c) 2021 Bryan Braun; Licensed MIT
6 | //
7 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
8 | !function(A,e){"use strict";"function"==typeof define&&define.amd?define([],e):"object"==typeof module&&module.exports?module.exports=e():(A.AnchorJS=e(),A.anchors=new A.AnchorJS)}(this,function(){"use strict";return function(A){function d(A){A.icon=Object.prototype.hasOwnProperty.call(A,"icon")?A.icon:"",A.visible=Object.prototype.hasOwnProperty.call(A,"visible")?A.visible:"hover",A.placement=Object.prototype.hasOwnProperty.call(A,"placement")?A.placement:"right",A.ariaLabel=Object.prototype.hasOwnProperty.call(A,"ariaLabel")?A.ariaLabel:"Anchor",A.class=Object.prototype.hasOwnProperty.call(A,"class")?A.class:"",A.base=Object.prototype.hasOwnProperty.call(A,"base")?A.base:"",A.truncate=Object.prototype.hasOwnProperty.call(A,"truncate")?Math.floor(A.truncate):64,A.titleText=Object.prototype.hasOwnProperty.call(A,"titleText")?A.titleText:""}function w(A){var e;if("string"==typeof A||A instanceof String)e=[].slice.call(document.querySelectorAll(A));else{if(!(Array.isArray(A)||A instanceof NodeList))throw new TypeError("The selector provided to AnchorJS was invalid.");e=[].slice.call(A)}return e}this.options=A||{},this.elements=[],d(this.options),this.isTouchDevice=function(){return Boolean("ontouchstart"in window||window.TouchEvent||window.DocumentTouch&&document instanceof DocumentTouch)},this.add=function(A){var e,t,o,i,n,s,a,c,r,l,h,u,p=[];if(d(this.options),"touch"===(l=this.options.visible)&&(l=this.isTouchDevice()?"always":"hover"),0===(e=w(A=A||"h2, h3, h4, h5, h6")).length)return this;for(null===document.head.querySelector("style.anchorjs")&&((u=document.createElement("style")).className="anchorjs",u.appendChild(document.createTextNode("")),void 0===(A=document.head.querySelector('[rel="stylesheet"],style'))?document.head.appendChild(u):document.head.insertBefore(u,A),u.sheet.insertRule(".anchorjs-link{opacity:0;text-decoration:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}",u.sheet.cssRules.length),u.sheet.insertRule(":hover>.anchorjs-link,.anchorjs-link:focus{opacity:1}",u.sheet.cssRules.length),u.sheet.insertRule("[data-anchorjs-icon]::after{content:attr(data-anchorjs-icon)}",u.sheet.cssRules.length),u.sheet.insertRule('@font-face{font-family:anchorjs-icons;src:url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype")}',u.sheet.cssRules.length)),u=document.querySelectorAll("[id]"),t=[].map.call(u,function(A){return A.id}),i=0;i\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}});
9 | // @license-end
--------------------------------------------------------------------------------
/docs/figures/logo/iMOD-tutorial.svg:
--------------------------------------------------------------------------------
1 |
2 |
81 |
--------------------------------------------------------------------------------
/gistim/geopackage.py:
--------------------------------------------------------------------------------
1 | """
2 | Utilities to write data to a geopackage.
3 | """
4 | import itertools
5 | import shutil
6 | import sqlite3
7 | from pathlib import Path
8 | from typing import Dict, List, NamedTuple, Tuple
9 |
10 | import numpy as np
11 | import pandas as pd
12 |
13 | from gistim.geomet import geopackage
14 |
15 |
16 | class CoordinateReferenceSystem(NamedTuple):
17 | description: str
18 | organization: str
19 | srs_id: int
20 | wkt: str
21 |
22 |
23 | class BoundingBox(NamedTuple):
24 | xmin: float
25 | ymin: float
26 | xmax: float
27 | ymax: float
28 |
29 |
30 | WGS84_WKT = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
31 | APPLICATION_ID = 1196444487
32 | USER_VERSION = 10200
33 |
34 |
35 | def create_gpkg_contents(
36 | table_names: List[str], bounding_boxes: List[BoundingBox], srs_id: int
37 | ) -> pd.DataFrame:
38 | # From records?
39 | return pd.DataFrame(
40 | data={
41 | "table_name": table_names,
42 | "data_type": "features",
43 | "identifier": table_names,
44 | "description": "",
45 | "last_change": pd.Timestamp.now(),
46 | "min_x": [bb.xmin for bb in bounding_boxes],
47 | "min_y": [bb.ymin for bb in bounding_boxes],
48 | "max_x": [bb.xmax for bb in bounding_boxes],
49 | "max_y": [bb.ymax for bb in bounding_boxes],
50 | "srs_id": srs_id,
51 | }
52 | )
53 |
54 |
55 | def create_gkpg_spatial_ref_sys(crs: CoordinateReferenceSystem) -> pd.DataFrame:
56 | return pd.DataFrame(
57 | data={
58 | "srs_name": [
59 | "Undefined Cartesian SRS",
60 | "Undefined geographic SRS",
61 | "WGS 84 geodetic",
62 | crs.description,
63 | ],
64 | "srs_id": [-1, 0, 4326, crs.srs_id],
65 | "organization": ["NONE", "NONE", "EPSG", "EPSG"],
66 | "organization_coordsys_id": [-1, 0, 4326, crs.organization],
67 | "definition": ["undefined", "undefined", WGS84_WKT, crs.wkt],
68 | "description": [
69 | "undefined Cartesian coordinate reference system",
70 | "undefined geographic coordinate reference system",
71 | "longitude/latitude coordinates in decimal degrees on the WGS 84 spheroid",
72 | "",
73 | ],
74 | }
75 | )
76 |
77 |
78 | def create_gpkg_geometry_columns(
79 | table_names: List[str],
80 | geometry_type_names: List[str],
81 | srs_id: int,
82 | ) -> pd.DataFrame:
83 | return pd.DataFrame(
84 | data={
85 | "table_name": table_names,
86 | "column_name": "geom",
87 | "geometry_type_name": geometry_type_names,
88 | "srs_id": srs_id,
89 | "z": 0,
90 | "m": 0,
91 | }
92 | )
93 |
94 |
95 | def collect_bounding_box(features, geometry_type) -> BoundingBox:
96 | if geometry_type == "Point":
97 | x = []
98 | y = []
99 | for point in features:
100 | coordinates = point["coordinates"]
101 | x.append(coordinates[0])
102 | y.append(coordinates[1])
103 | else:
104 | x, y = zip(
105 | *itertools.chain.from_iterable(line["coordinates"] for line in features)
106 | )
107 | return BoundingBox(xmin=min(x), ymin=min(y), xmax=max(x), ymax=max(y))
108 |
109 |
110 | def process_table(dataframe: pd.DataFrame) -> Tuple[pd.DataFrame, BoundingBox, str]:
111 | geometry = dataframe.pop("geometry").to_numpy()
112 | geometry_type = set(f["type"] for f in geometry)
113 | if len(geometry_type) != 1:
114 | raise ValueError(
115 | f"Table should contain exactly one geometry type. Received: {geometry_type}"
116 | )
117 | # Get first (and only) member of set.
118 | geometry_type = next(iter(geometry_type))
119 | bounding_box = collect_bounding_box(geometry, geometry_type)
120 | dataframe["geom"] = [geopackage.dumps(f) for f in geometry]
121 | return dataframe, bounding_box, geometry_type
122 |
123 |
124 | def force_sql_datetime(df: pd.DataFrame):
125 | """
126 | Pandas SQL writes datetimes as SQL Timestamps, which are not accepted by QGIS.
127 | They have to SQL DATETIME instead.
128 | """
129 | return {
130 | colname: "DATETIME"
131 | for colname, dtype in df.dtypes.to_dict().items()
132 | if np.issubdtype(dtype, np.datetime64)
133 | }
134 |
135 |
136 | def write_geopackage(
137 | tables: Dict[str, pd.DataFrame], crs: CoordinateReferenceSystem, path: Path
138 | ) -> None:
139 | """
140 | Write the content of the tables to a geopackage. Overwrites the geopackage
141 | if it already exists.
142 |
143 | Parameters
144 | ----------
145 | tables: Dict[str, pd.DataFrame]
146 | Mapping of table name to contents, as a dataframe.
147 | May be an empty dictionary.
148 | crs: CoordinateReferenceSystem
149 | Coordinate reference system of the geometries.
150 | path: Path
151 | Path to the geopackage to write.
152 |
153 | Returns
154 | -------
155 | None
156 | """
157 | # We write all tables to a temporary GeoPackage with a dot prefix, and at
158 | # the end move this over the target file. This does not throw a
159 | # PermissionError if the file is open in QGIS.
160 | gpkg_path = path.with_suffix(".output.gpkg")
161 | temp_path = gpkg_path.with_stem(".")
162 | # avoid adding tables to existing geopackage.
163 | temp_path.unlink(missing_ok=True)
164 |
165 | try:
166 | connection = sqlite3.connect(database=temp_path)
167 | connection.execute(f"PRAGMA application_id = {APPLICATION_ID};")
168 | connection.execute(f"PRAGMA user_version = {USER_VERSION};")
169 |
170 | table_names = []
171 | geometry_types = []
172 | bounding_boxes = []
173 | for layername, dataframe in tables.items():
174 | dataframe, bounding_box, geometry_type = process_table(dataframe)
175 | table_names.append(layername)
176 | geometry_types.append(geometry_type)
177 | bounding_boxes.append(bounding_box)
178 | dataframe.to_sql(
179 | layername, con=connection, dtype=force_sql_datetime(dataframe)
180 | )
181 |
182 | # Create mandatory geopackage tables.
183 | gpkg_contents = create_gpkg_contents(
184 | table_names=table_names, bounding_boxes=bounding_boxes, srs_id=crs.srs_id
185 | )
186 | gpkg_geometry_columns = create_gpkg_geometry_columns(
187 | table_names=table_names,
188 | geometry_type_names=geometry_types,
189 | srs_id=crs.srs_id,
190 | )
191 | gpkg_spatial_ref_sys = create_gkpg_spatial_ref_sys(crs)
192 | # Write to Geopackage database.
193 | gpkg_contents.to_sql(name="gpkg_contents", con=connection)
194 | gpkg_geometry_columns.to_sql(name="gpkg_geometry_columns", con=connection)
195 | gpkg_spatial_ref_sys.to_sql(name="gpkg_spatial_ref_sys", con=connection)
196 |
197 | finally:
198 | connection.commit()
199 | connection.close()
200 |
201 | shutil.move(temp_path, gpkg_path)
202 | return
203 |
--------------------------------------------------------------------------------
/plugin/qgistim/widgets/install_dialog.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import Qt
2 | from PyQt5.QtWidgets import (
3 | QFileDialog,
4 | QDialog,
5 | QPushButton,
6 | QVBoxLayout,
7 | QHBoxLayout,
8 | QLabel,
9 | QLineEdit,
10 | QMessageBox,
11 | QGroupBox,
12 | QGridLayout,
13 | )
14 | from qgis.core import Qgis, QgsTask, QgsApplication
15 | from qgistim.core.install_backend import install_from_github, install_from_zip
16 |
17 |
18 | class InstallTask(QgsTask):
19 | def finished(self, result) -> None:
20 | self.parent.enable_install_buttons(True)
21 | self.parent.install_zip_line_edit.setText("")
22 | self.parent.update_versions()
23 | if result:
24 | self.message_bar.pushMessage(
25 | title="Info",
26 | text="Succesfully installed TimML and TTim server",
27 | level=Qgis.Info,
28 | )
29 | else:
30 | if self.exception is not None:
31 | message = "Exception: " + str(self.exception)
32 | else:
33 | message = "Unknown failure"
34 |
35 | self.message_bar.pushMessage(
36 | title="Error",
37 | text=f"Failed to install TimML and TTim server. {message}",
38 | level=Qgis.Critical,
39 | )
40 | return
41 |
42 | def cancel(self) -> None:
43 | self.parent.enable_install_buttons(True)
44 | super().cancel()
45 | return
46 |
47 |
48 | class InstallZipTask(InstallTask):
49 | def __init__(self, parent, path: str, message_bar):
50 | super().__init__("Install from ZIP file", QgsTask.CanCancel)
51 | self.parent = parent
52 | self.path = path
53 | self.message_bar = message_bar
54 | self.exception = None
55 |
56 | def run(self) -> bool:
57 | try:
58 | install_from_zip(self.path)
59 | return True
60 | except Exception as exception:
61 | self.exception = exception
62 | return False
63 |
64 |
65 | class InstallGithubTask(InstallTask):
66 | def __init__(self, parent, message_bar):
67 | super().__init__("Install from GitHub", QgsTask.CanCancel)
68 | self.parent = parent
69 | self.message_bar = message_bar
70 | self.exception = None
71 |
72 | def run(self) -> bool:
73 | try:
74 | install_from_github()
75 | return True
76 | except Exception as exception:
77 | self.exception = exception
78 | return False
79 |
80 |
81 | class InstallDialog(QDialog):
82 | """
83 | Download and install from GitHub, or install from ZIP file.
84 | """
85 |
86 | def __init__(self, parent=None):
87 | QDialog.__init__(self, parent)
88 | self.parent = parent
89 | self.setWindowTitle("Install TimML and TTim server")
90 | self.install_task = None
91 |
92 | # Define widgets
93 | self.install_github_button = QPushButton("Install latest release from GitHub")
94 | self.set_zip_button = QPushButton("Browse...")
95 | self.install_zip_button = QPushButton("Install")
96 | self.install_zip_line_edit = QLineEdit()
97 | self.install_zip_line_edit.setMinimumWidth(400)
98 | self.close_button = QPushButton("Close")
99 |
100 | # Connect with actions
101 | self.install_github_button.clicked.connect(self.install_from_github)
102 | self.set_zip_button.clicked.connect(self.set_zip_path)
103 | self.install_zip_button.clicked.connect(self.install_from_zip)
104 | self.close_button.clicked.connect(self.reject)
105 |
106 | # Set layout
107 | github_row = QHBoxLayout()
108 | github_row.addWidget(self.install_github_button)
109 | zip_row = QHBoxLayout()
110 | zip_row.addWidget(QLabel("ZIP file"))
111 | zip_row.addWidget(self.install_zip_line_edit)
112 | zip_row.addWidget(self.set_zip_button)
113 | zip_row.addWidget(self.install_zip_button)
114 |
115 | self.version_widgets, version_layout = self.initialize_version_view()
116 | self.update_versions()
117 | version_group = QGroupBox("Current Versions")
118 | version_group.setLayout(version_layout)
119 | github_group = QGroupBox("Install from GitHub")
120 | github_group.setLayout(github_row)
121 | zip_group = QGroupBox("Install from ZIP file")
122 | zip_group.setLayout(zip_row)
123 |
124 | layout = QVBoxLayout()
125 | layout.addWidget(version_group)
126 | layout.addWidget(github_group)
127 | layout.addWidget(zip_group)
128 | layout.addWidget(self.close_button, stretch=0, alignment=Qt.AlignRight)
129 | layout.addStretch()
130 | self.setLayout(layout)
131 |
132 | def initialize_version_view(self):
133 | version_widgets = {}
134 | version_layout = QGridLayout()
135 | for i, package in enumerate(["timml", "ttim", "gistim"]):
136 | version_view = QLineEdit()
137 | version_view.setEnabled(False)
138 | widgets = (
139 | QLabel(package),
140 | version_view,
141 | )
142 | version_widgets[package] = widgets
143 | for j, widget in enumerate(widgets):
144 | version_layout.addWidget(widget, i, j)
145 | return version_widgets, version_layout
146 |
147 | def update_versions(self):
148 | versions = self.parent.server_handler.versions()
149 | for package in ["timml", "ttim", "gistim"]:
150 | self.version_widgets[package][1].setText(versions.get(package))
151 | return
152 |
153 | def enable_install_buttons(self, enabled: bool) -> None:
154 | self.install_github_button.setEnabled(enabled)
155 | self.install_zip_button.setEnabled(enabled)
156 | return
157 |
158 | def set_zip_path(self) -> None:
159 | path, _ = QFileDialog.getOpenFileName(self, "Select file", "", "*.zip")
160 | if path != "": # Empty string in case of cancel button press
161 | self.install_zip_line_edit.setText(path)
162 | return
163 |
164 | def install_from_zip(self) -> None:
165 | path = self.install_zip_line_edit.text()
166 | if path == "":
167 | return
168 |
169 | reply = QMessageBox.question(
170 | self,
171 | "Install from ZIP?",
172 | "This will install from the selected ZIP file. Continue?",
173 | QMessageBox.Yes | QMessageBox.No,
174 | QMessageBox.No,
175 | )
176 | if reply == QMessageBox.No:
177 | return
178 | self.install_task = InstallZipTask(
179 | self, path=path, message_bar=self.parent.message_bar
180 | )
181 | self.enable_install_buttons(False)
182 | QgsApplication.taskManager().addTask(self.install_task)
183 | return
184 |
185 | def install_from_github(self) -> None:
186 | reply = QMessageBox.question(
187 | self,
188 | "Install from Github?",
189 | "This will download and install the latest release from GitHub. Continue?",
190 | QMessageBox.Yes | QMessageBox.No,
191 | QMessageBox.No,
192 | )
193 | if reply == QMessageBox.No:
194 | return
195 | self.install_task = InstallGithubTask(self, message_bar=self.parent.message_bar)
196 | self.enable_install_buttons(False)
197 | QgsApplication.taskManager().addTask(self.install_task)
198 | return
199 |
--------------------------------------------------------------------------------