├── 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}

") 18 | else: 19 | messages.append(f"

{variable}

") 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 | ![The welcome screen](figures/deltaforge/1_welcome_screen.png) 18 | 19 | Click \"Next\", and then \"I agree\" in the license agreement. 20 | 21 | ![License agreement screen](figures/deltaforge/2_license.png) 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 | ![The installation type 29 | screen](figures/deltaforge/3_installation_type.png) 30 | 31 | Next you get to decide where the python environment is installed. The 32 | default location is usually fine. 33 | 34 | ![The location of the python 35 | installation](figures/deltaforge/4_install_location.png) 36 | 37 | Finally, some further configuration is possible. The screenshots 38 | contains the options we recommend. 39 | 40 | ![Installation options with the recommended options 41 | selected.](figures/deltaforge/5_installation_options.png) 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 | ![The Deltaforge Prompt should be findable in the Windows start 50 | menu](figures/deltaforge/6_deltaforge_start_menu.png) 51 | 52 | This will start a command prompt screen (`cmd.exe`), where at startup 53 | the Deltaforge python environment is activated. 54 | 55 | ![The Deltaforge prompt. You can type `mamba list` to view all the 56 | packages installed.](figures/deltaforge/7_deltaforge_prompt.png) 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). ![](figures/tutorial/button-Qgis-tim.png){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: " 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 | 20 | 22 | 45 | 48 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 65 | 71 | 77 | 82 | 83 | 89 | 95 | 101 | 102 | 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 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 33 | 35 | 56 | 62 | 67 | 68 | 72 | 76 | 80 | 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 | --------------------------------------------------------------------------------