├── img
├── icon.png
├── info.png
├── new.png
├── save.png
├── close.png
├── folder.png
├── radar.png
├── regnie.png
├── search.png
├── stack.png
├── stats.png
├── execute.png
├── qgis_logo.png
├── sw_info.png
├── plugin_logo.png
├── sw_download.png
└── weihnachten.gif
├── example
├── visualization.jpg
├── shapes
│ ├── DEU_adm0
│ │ ├── DEU_adm0.dbf
│ │ ├── DEU_adm0.sbn
│ │ ├── DEU_adm0.sbx
│ │ ├── DEU_adm0.shp
│ │ ├── DEU_adm0.shx
│ │ ├── DEU_adm0.prj
│ │ └── DEU_readme.txt
│ ├── DEU_adm1
│ │ ├── DEU_adm1.dbf
│ │ ├── DEU_adm1.sbn
│ │ ├── DEU_adm1.sbx
│ │ ├── DEU_adm1.shp
│ │ ├── DEU_adm1.shx
│ │ ├── DEU_adm1.prj
│ │ └── DEU_readme.txt
│ ├── RadarNetwork
│ │ ├── radarloc.gpkg
│ │ └── radarbuffer_150km.gpkg
│ ├── state_capitals_germany.gpkg
│ └── Hessen
│ │ ├── DE_bundeslaender_GEN_Hessen.dbf
│ │ ├── DE_bundeslaender_GEN_Hessen.shp
│ │ ├── DE_bundeslaender_GEN_Hessen.shx
│ │ ├── DE_bundeslaender_GEN_Hessen.prj
│ │ └── DE_bundeslaender_GEN_Hessen.qpj
└── sample_file
│ └── raa01-rw_10000-1708020250-dwd---bin.gz
├── resources.qrc
├── .gitignore
├── tests
└── test_env.py
├── config.ini
├── classes
├── def_projections.py
├── regnie2raster.py
├── ASCIIGridWriter.py
├── Regnie.py
├── SettingsTab.py
├── ActionTabRegnie.py
├── def_products.py
├── ActionTabBase.py
├── gui.py
├── NumpyRadolanAdder.py
├── ActionTabRADOLANAdder.py
├── LayerLoader.py
├── Model.py
└── GDALProcessing.py
├── .flake8
├── symbology
├── BJ.qml
├── rvp6units.qml
├── YU.qml
├── YR.qml
├── YA.qml
├── YB.qml
├── YW.qml
├── daily+.qml
├── daily.qml
├── hourly.qml
├── 5min.qml
├── yearly.qml
├── monthly.qml
├── HG.qml
├── regnie_raster_daily.qml
├── regnie_raster_yearly.qml
├── regnie_raster_monthly.qml
└── WN.qml
├── .github
└── workflows
│ └── python-app.yml
├── __init__.py
├── pb_tool.cfg
├── README.md
└── metadata.txt
/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/icon.png
--------------------------------------------------------------------------------
/img/info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/info.png
--------------------------------------------------------------------------------
/img/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/new.png
--------------------------------------------------------------------------------
/img/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/save.png
--------------------------------------------------------------------------------
/img/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/close.png
--------------------------------------------------------------------------------
/img/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/folder.png
--------------------------------------------------------------------------------
/img/radar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/radar.png
--------------------------------------------------------------------------------
/img/regnie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/regnie.png
--------------------------------------------------------------------------------
/img/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/search.png
--------------------------------------------------------------------------------
/img/stack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/stack.png
--------------------------------------------------------------------------------
/img/stats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/stats.png
--------------------------------------------------------------------------------
/img/execute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/execute.png
--------------------------------------------------------------------------------
/img/qgis_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/qgis_logo.png
--------------------------------------------------------------------------------
/img/sw_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/sw_info.png
--------------------------------------------------------------------------------
/img/plugin_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/plugin_logo.png
--------------------------------------------------------------------------------
/img/sw_download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/sw_download.png
--------------------------------------------------------------------------------
/img/weihnachten.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/img/weihnachten.gif
--------------------------------------------------------------------------------
/example/visualization.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/visualization.jpg
--------------------------------------------------------------------------------
/example/shapes/DEU_adm0/DEU_adm0.dbf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm0/DEU_adm0.dbf
--------------------------------------------------------------------------------
/example/shapes/DEU_adm0/DEU_adm0.sbn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm0/DEU_adm0.sbn
--------------------------------------------------------------------------------
/example/shapes/DEU_adm0/DEU_adm0.sbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm0/DEU_adm0.sbx
--------------------------------------------------------------------------------
/example/shapes/DEU_adm0/DEU_adm0.shp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm0/DEU_adm0.shp
--------------------------------------------------------------------------------
/example/shapes/DEU_adm0/DEU_adm0.shx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm0/DEU_adm0.shx
--------------------------------------------------------------------------------
/example/shapes/DEU_adm1/DEU_adm1.dbf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm1/DEU_adm1.dbf
--------------------------------------------------------------------------------
/example/shapes/DEU_adm1/DEU_adm1.sbn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm1/DEU_adm1.sbn
--------------------------------------------------------------------------------
/example/shapes/DEU_adm1/DEU_adm1.sbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm1/DEU_adm1.sbx
--------------------------------------------------------------------------------
/example/shapes/DEU_adm1/DEU_adm1.shp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm1/DEU_adm1.shp
--------------------------------------------------------------------------------
/example/shapes/DEU_adm1/DEU_adm1.shx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/DEU_adm1/DEU_adm1.shx
--------------------------------------------------------------------------------
/example/shapes/RadarNetwork/radarloc.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/RadarNetwork/radarloc.gpkg
--------------------------------------------------------------------------------
/example/shapes/state_capitals_germany.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/state_capitals_germany.gpkg
--------------------------------------------------------------------------------
/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | img/icon.png
4 |
5 |
6 |
--------------------------------------------------------------------------------
/example/shapes/RadarNetwork/radarbuffer_150km.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/RadarNetwork/radarbuffer_150km.gpkg
--------------------------------------------------------------------------------
/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.dbf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.dbf
--------------------------------------------------------------------------------
/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.shp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.shp
--------------------------------------------------------------------------------
/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.shx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.shx
--------------------------------------------------------------------------------
/example/sample_file/raa01-rw_10000-1708020250-dwd---bin.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Weathermann/radolan2map/HEAD/example/sample_file/raa01-rw_10000-1708020250-dwd---bin.gz
--------------------------------------------------------------------------------
/example/shapes/DEU_adm0/DEU_adm0.prj:
--------------------------------------------------------------------------------
1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
--------------------------------------------------------------------------------
/example/shapes/DEU_adm1/DEU_adm1.prj:
--------------------------------------------------------------------------------
1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python compiled
2 | __pycache__
3 | classes/__pycache__
4 | .idea
5 | settings.json
6 | resources.py
7 | # pb_tools zip build
8 | zip_build/
9 | # VS Code
10 | .vscode
11 | news.txt
12 |
--------------------------------------------------------------------------------
/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.prj:
--------------------------------------------------------------------------------
1 | PROJCS["Stereographic_North_Pole",GEOGCS["GCS_unnamed ellipse",DATUM["D_unknown",SPHEROID["Unknown",6370040,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Stereographic_North_Pole"],PARAMETER["standard_parallel_1",60],PARAMETER["central_meridian",10],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1]]
--------------------------------------------------------------------------------
/example/shapes/Hessen/DE_bundeslaender_GEN_Hessen.qpj:
--------------------------------------------------------------------------------
1 | PROJCS["unnamed",GEOGCS["unnamed ellipse",DATUM["unknown",SPHEROID["unnamed",6370040,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",60],PARAMETER["central_meridian",10],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1]]
2 |
--------------------------------------------------------------------------------
/example/shapes/DEU_adm0/DEU_readme.txt:
--------------------------------------------------------------------------------
1 | These files were extracted from GADM version 1.0, in March 2009
2 |
3 | GADM is a geographic database of global administrative areas (boundaries).
4 |
5 | This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.
6 | This only covers our contribution, not that of others (data provided to us).
7 |
8 | Visit http://www.gadm.org for details
9 |
10 |
--------------------------------------------------------------------------------
/example/shapes/DEU_adm1/DEU_readme.txt:
--------------------------------------------------------------------------------
1 | These files were extracted from GADM version 1.0, in March 2009
2 |
3 | GADM is a geographic database of global administrative areas (boundaries).
4 |
5 | This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.
6 | This only covers our contribution, not that of others (data provided to us).
7 |
8 | Visit http://www.gadm.org for details
9 |
10 |
--------------------------------------------------------------------------------
/tests/test_env.py:
--------------------------------------------------------------------------------
1 | # test_env.py
2 |
3 | import os
4 | import subprocess
5 | import platform
6 |
7 |
8 | def test_print_platform():
9 | print(platform.platform())
10 | print("Cores:", os.cpu_count())
11 | mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") # total physical memory in Bytes
12 | print("Mem:", mem)
13 |
14 | def test_system():
15 | cmd = "cat /etc/os-release"
16 | ret = subprocess.call(cmd, shell=True)
17 | assert ret == 0
18 |
--------------------------------------------------------------------------------
/config.ini:
--------------------------------------------------------------------------------
1 | # Paths configuration for 'radolan2map'
2 |
3 | [Paths]
4 |
5 | ; Unterverzeichnisse / Subdirs in "plugin dir":
6 | STYLE_PATH = symbology
7 | BASE_DATA_PATH = base_data
8 | DEFAULT_PROJECT = example/project_template.qgz.qgs
9 | print_layout = example/print_layout.qpt
10 | ; default cut to this shape file:
11 | CUT_TO = example/shapes/DEU_adm0/DEU_adm0.shp
12 |
13 | # you can define your own logo here for usage in print layout:
14 | #logo_image = .../radolan2map/img/plugin_logo.png
15 |
16 | datadir_deffile_basename = data_path.conf
17 | last_products_basename = last.conf
18 |
19 |
--------------------------------------------------------------------------------
/classes/def_projections.py:
--------------------------------------------------------------------------------
1 | """
2 | Created on 21.11.2019
3 | """
4 |
5 | from collections import OrderedDict
6 |
7 |
8 | # int: String
9 | # EPSG code, projection description
10 | projs = OrderedDict()
11 |
12 | # The projections are inserted in the projection combo box in the specified order.
13 | # Key: number, Value: description, optional: projection parameters - value must be list type then.
14 | projs[3035] = "EPSG 3035: ETRS89 / LAEA Europe"
15 | projs[3857] = "EPSG 3857: Web Mercator / Pseudo Mercator"
16 | projs[4326] = "EPSG 4326: WGS84 / World Geodetic System 1984"
17 |
18 | # classical RADOLAN products, earth as sphere
19 | """ With QGIS 3.10.1-A Coruña on openSUSE Linux 15.1 this projection
20 | wasn't processed by gdal.Warp() with GDAL version 3.0.2 any more; maybe 'k' is the problem:
21 | '+proj=stere +lat_0=90 +lat_ts=60 +lon_0=10 +k=0.93301270189 +x_0=0 +y_0=0 +a=6370040 +b=6370040 +units=m +no_defs' """
22 | projs[0] = ["RADOLAN polarstereographic projection",
23 | '+proj=stere +lat_0=90 +lat_ts=60 +lon_0=10 +a=6370040 +b=6370040 +units=m +no_defs']
24 | # 2022: POLARA generated products(?), earth as WGS ellipsoid
25 | projs[1] = ["polarstereographic WGS projection",
26 | '+proj=stere +lat_0=90 +lat_ts=60 +lon_0=10 +a=6378137 +b=6356752.3142451802 +units=m +no_defs']
27 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | .git,
4 | .gitignore,
5 | .vscode,
6 | __pycache__,
7 | zip_build,
8 | example,
9 | img,
10 | symbology,
11 | resources.py,
12 | ignore =
13 | # C901 # function is too complex (mccabe)
14 | # E114, # indentation is not a multiple of four (comment)
15 | # E117, # over-indented (comment)
16 | # E122, # continuation line missing indentation or outdented
17 | # E203, # whitespace before ','
18 | # E226, # missing whitespace around arithmetic operator
19 | # E241, # multiple spaces after ','
20 | # E242, # tab after ','
21 | # E251, # unexpected spaces around keyword / parameter equals
22 | # E302, # expected 2 blank lines, found 1
23 | # E303, # too many blank lines (2)
24 | # E501, # line too long (> 175 characters)
25 | # E701, # multiple statements on one line (colon)
26 | # E722, # do not use bare 'except'
27 | # E999, # SyntaxError: invalid syntax
28 | # F401, # imported but unused
29 | # F541, # f-string is missing placeholders
30 | # F841, # local variable is assigned to but never used
31 | # W291, # trailing whitespace
32 | # W293, # blank line contains whitespace
33 | statistics = True
34 | count = True
35 | max-line-length = 175
36 | max-complexity=5
37 |
--------------------------------------------------------------------------------
/symbology/BJ.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 0
23 |
24 |
--------------------------------------------------------------------------------
/.github/workflows/python-app.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: Python application
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | build:
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up Python 3.10
23 | uses: actions/setup-python@v3
24 | with:
25 | python-version: "3.10"
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | pip install flake8 pytest
30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31 | # verschiedene nicht-existente Variablen/Methoden im NumpyRadolanReader,
32 | # daher flake erstmal deaktivieren:
33 | #- name: Lint with flake8
34 | # run: |
35 | # # stop the build if there are Python syntax errors or undefined names
36 | # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
37 | # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
38 | # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
39 | - name: Test with pytest
40 | run: |
41 | #pytest
42 | pytest -v
43 |
--------------------------------------------------------------------------------
/symbology/rvp6units.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | None
19 | WholeRaster
20 | Estimated
21 | 0.02
22 | 0.98
23 | 2
24 |
25 |
26 | 0
27 | 249
28 | StretchToMinimumMaximum
29 |
30 |
31 |
32 |
33 |
34 |
35 | 0
36 |
37 |
--------------------------------------------------------------------------------
/symbology/YU.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 0
27 |
28 |
--------------------------------------------------------------------------------
/symbology/YR.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 0
27 |
28 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | This script initializes the plugin, making it known to QGIS.
5 |
6 | Radolan2Map
7 | A QGIS plugin
8 | A QGIS plugin to bring a RADOLAN binary file onto a map
9 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
10 | -------------------
11 | begin : 2019-08-05
12 | copyright : (C) 2019 by Weatherman
13 | email : radolan2map@e.mail.de
14 | git sha : $Format:%H$
15 | ***************************************************************************/
16 |
17 | /***************************************************************************
18 | * *
19 | * This program is free software; you can redistribute it and/or modify *
20 | * it under the terms of the GNU General Public License as published by *
21 | * the Free Software Foundation; either version 2 of the License, or *
22 | * (at your option) any later version. *
23 | * *
24 | ***************************************************************************/
25 | """
26 |
27 |
28 | # noinspection PyPep8Naming
29 | def classFactory(iface): # pylint: disable=invalid-name
30 | """Load Radolan2Map class from file Radolan2Map.
31 |
32 | :param iface: A QGIS interface instance.
33 | :type iface: QgsInterface
34 | """
35 | #
36 | from .radolan2map import Radolan2Map
37 | return Radolan2Map(iface)
38 |
--------------------------------------------------------------------------------
/symbology/YA.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 0
27 |
28 |
--------------------------------------------------------------------------------
/symbology/YB.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 0
27 |
28 |
--------------------------------------------------------------------------------
/symbology/YW.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 0
28 |
29 |
--------------------------------------------------------------------------------
/symbology/daily+.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 0
29 |
30 |
--------------------------------------------------------------------------------
/pb_tool.cfg:
--------------------------------------------------------------------------------
1 | # Configuration file for plugin builder tool
2 | # Sane defaults for your plugin generated by the Plugin Builder are
3 | # already set below.
4 | #
5 | # As you add Python source files and UI files to your plugin, add
6 | # them to the appropriate [files] section below.
7 |
8 | [plugin]
9 | # Name of the plugin. This is the name of the directory that will
10 | # be created when deployed
11 | name: radolan2map
12 |
13 | # Full path to where you want your plugin directory copied. If empty,
14 | # the QGIS default path will be used. Don't include the plugin name in
15 | # the path.
16 | # plugin_path: /home/USER/.local/share/QGIS/QGIS3/profiles/default/python/plugins
17 | # plugin_path: c:\Users\USER\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins
18 | # plugin_path: /Users/USER/Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins
19 |
20 | [files]
21 | # Python files that should be deployed with the plugin
22 | python_files: radolan2map.py __init__.py
23 |
24 | # The main dialog file that is loaded (not compiled)
25 | main_dialog: dock_widget.ui
26 |
27 | # Other ui files for your dialogs (these will be compiled)
28 | compiled_ui_files:
29 |
30 | # Resource file(s) that will be compiled
31 | resource_files: resources.qrc
32 |
33 | # Other files required for the plugin
34 | extras: metadata.txt config.ini
35 |
36 | # Other directories to be deployed with the plugin.
37 | # These must be subdirectories under the plugin directory
38 | extra_dirs: classes example img symbology
39 |
40 | # ISO code(s) for any locales (translations), separated by spaces.
41 | # Corresponding .ts files must exist in the i18n directory
42 | locales:
43 |
44 | # Uncomment the following to include help in the deployment
45 | # [help]
46 | # # the built help directory that should be deployed with the plugin
47 | # dir: help/build/html
48 | # # the name of the directory to target in the deployed plugin
49 | # target: help
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # radolan2map
2 |
3 | **QGIS plugin for DWD rasterized precipitation datasets like RADOLAN/RADKLIM and REGNIE**
4 | This project is a QGIS plugin that brings precipitation data from German Meteorological Service (DWD, https://www.dwd.de) - radar data in RADOLAN/RADKLIM binary format in any grid dimension as well as REGNIE - on a map. You also can **add** up RADOLAN data!
5 | Applications: hydrometeorological and agricultural analyses.
6 |
7 | ### Features
8 | load the following data types:
9 | - load/display any binary file in RADOLAN / RADKLIM format (incl. gz-compression)
10 | - radolan summation: you can add up RADOLAN data!
11 | - daily, monthly and multiannual REGNIE files (incl. gz-compression)
12 |
13 | additional functions for RADOLAN:
14 | - bring it to standard projection [ETRS89 / LAEA Europe](https://epsg.io/3035)
15 | - optional cut to german border (or any other shape file)
16 | - status of DWD Radar Network included (see [Wiki](https://gitlab.com/Weatherman_/radolan2map/wikis/home))
17 |
18 |
19 | ### Installation, Usage
20 | => [Wiki](https://gitlab.com/Weatherman_/radolan2map/wikis/home)
21 |
22 |
23 | ### Data info
24 | #### RADOLAN
25 | * Overview: [https://dwd.de/RADOLAN](https://dwd.de/RADOLAN). There are hourly (RH, RW) or daily (SF) datasets.
26 | A RADOLAN-RW-testfile (gzipped binary) is included in directory `example/sample_file/`.
27 | * Data download: RADOLAN (recent) and RADKLIM radar climatology dataset:
28 | [DWD open climate data](https://opendata.dwd.de/climate_environment/CDC/grids_germany/hourly/radolan/)
29 | #### REGNIE
30 | * REGNIE overview: https://www.dwd.de/DE/leistungen/regnie/regnie.html
31 | * Data download:
32 | * [REGNIE daily data](https://opendata.dwd.de/climate_environment/CDC/grids_germany/daily/regnie/)
33 | * [REGNIE monthly data](https://opendata.dwd.de/climate_environment/CDC/grids_germany/monthly/regnie/)
34 | * [REGNIE multiannual data](https://opendata.dwd.de/climate_environment/CDC/grids_germany/multi_annual/regnie/)
35 |
36 | ### Platform info
37 | Plugin was successfully tested with QGIS
38 | * 3.16 *Hannover* on Linux openSUSE 15.3 and Windows 10
39 | * 3.10 *A Coruña* on Linux openSUSE 15.1 and Windows 10
40 |
41 | If you experience any problems when starting the plugin, please make sure that
42 | you have a **current version of QGIS and Python installed** on your system.
43 |
44 |
45 | ### QGIS
46 | * [https://qgis.org](https://qgis.org)
47 | * [`radolan2map`](http://plugins.qgis.org/plugins/radolan2map/) at qgis.org
48 |
49 | ### Feel invited...
50 | to give me feedback about usability and
51 | help improving this plugin to promote open source (GIS)software and data!
52 |
53 |
--------------------------------------------------------------------------------
/symbology/daily.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | None
19 | WholeRaster
20 | Exact
21 | 0.02
22 | 0.98
23 | 2
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 0
52 |
53 |
--------------------------------------------------------------------------------
/symbology/hourly.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | None
19 | WholeRaster
20 | Exact
21 | 0.02
22 | 0.98
23 | 2
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 0
52 |
53 |
--------------------------------------------------------------------------------
/symbology/5min.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | None
19 | WholeRaster
20 | Exact
21 | 0.02
22 | 0.98
23 | 2
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 0
52 |
53 |
--------------------------------------------------------------------------------
/symbology/yearly.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | None
19 | WholeRaster
20 | Exact
21 | 0.02
22 | 0.98
23 | 2
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | 0
56 |
57 |
--------------------------------------------------------------------------------
/symbology/monthly.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | None
19 | WholeRaster
20 | Exact
21 | 0.02
22 | 0.98
23 | 2
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 0
57 |
58 |
--------------------------------------------------------------------------------
/symbology/HG.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 | 0
8 |
9 |
10 |
11 | 2022-12-10T18:56:00Z
12 | 2022-12-10T19:00:00Z
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | None
38 | WholeRaster
39 | Estimated
40 | 0.02
41 | 0.98
42 | 2
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | resamplingFilter
63 |
64 | 0
65 |
66 |
--------------------------------------------------------------------------------
/symbology/regnie_raster_daily.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | None
28 | WholeRaster
29 | Exact
30 | 0.02
31 | 0.98
32 | 2
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | resamplingFilter
63 |
64 | 0
65 |
66 |
--------------------------------------------------------------------------------
/symbology/regnie_raster_yearly.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | None
28 | WholeRaster
29 | Exact
30 | 0.02
31 | 0.98
32 | 2
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | resamplingFilter
63 |
64 | 0
65 |
66 |
--------------------------------------------------------------------------------
/symbology/regnie_raster_monthly.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | None
28 | WholeRaster
29 | Exact
30 | 0.02
31 | 0.98
32 | 2
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | resamplingFilter
63 |
64 | 0
65 |
66 |
--------------------------------------------------------------------------------
/classes/regnie2raster.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | """
3 | Converts a REGNIE file to a raster file (geotiff)
4 |
5 | This script can handle daily, monthly and multiannual REGNIE files
6 |
7 | Monthly and multiannual REGNIE files contain values in the unit mm and are converted to Int32 rasters with a NoData value of -999
8 | Daily REGNIE files contain values in the unit 1/10 mm, which are first converted to mm and then converted to Float64 rasters with a default NoData value
9 |
10 | REGNIE description: https://www.dwd.de/DE/leistungen/regnie/regnie.html
11 |
12 | REGNIE files are freely available from the DWD open data platform:
13 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/daily/regnie/
14 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/monthly/regnie/
15 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/multi_annual/regnie/
16 |
17 | @author: Felix Froehlich
18 | """
19 | import argparse
20 | from pathlib import Path
21 |
22 | import numpy as np
23 | from osgeo import gdal
24 | from osgeo import osr
25 |
26 | from .Regnie import Regnie
27 |
28 | def regnie2raster(file_regnie: Path, file_raster: Path) -> None:
29 | """
30 | Converts a REGNIE file to a raster file (geotiff)
31 |
32 | :param file_regnie: input REGNIE file
33 | :param file_raster: output raster file
34 | """
35 | # get array values from regnie file
36 | rg = Regnie(file_regnie)
37 |
38 | # define pixel size and origin
39 | # from https://opendata.dwd.de/climate_environment/CDC/grids_germany/daily/regnie/REGNIE_Beschreibung_20201109.pdf
40 | # (also applies to monthly regnie files)
41 | x_delta = 1.0 / 60.0
42 | y_delta = 1.0 / 120.0
43 | x_min = 6.0 - (10.0 * x_delta)
44 | y_max = (55.0 + 10.0 * y_delta)
45 | origin = (x_min - x_delta / 2, y_max + y_delta / 2) # include half cell offset
46 |
47 | # save as raster
48 | array2raster(file_raster, origin, x_delta, y_delta, rg.data)
49 |
50 | def array2raster(outfile: Path, rasterOrigin: tuple, pixelWidth: int, pixelHeight: int, array: np.array) -> None:
51 | """
52 | writes a numpy array to a raster file (geotiff)
53 | adapted from https://pcjericks.github.io/py-gdalogr-cookbook/raster_layers.html#create-raster-from-array
54 |
55 | :param outfile: path to output raster file
56 | :param rasterOrigin: tuple (x, y) of the top left coordinates of the raster
57 | :param pixelWidth: pixel width
58 | :param pixelHeight: pixel height
59 | :param array: two-dimensional numpy array with cell values
60 | """
61 |
62 | cols = array.shape[1]
63 | rows = array.shape[0]
64 | originX = rasterOrigin[0]
65 | originY = rasterOrigin[1]
66 |
67 | # set pixel data type depending on array dtype
68 | # available pixel data types: https://naturalatlas.github.io/node-gdal/classes/Constants%20(GDT).html
69 | # TODO: add more types
70 | if array.dtype == np.dtype(np.int32):
71 | typ = gdal.GDT_Int32
72 | else:
73 | typ = gdal.GDT_Float64
74 |
75 | driver = gdal.GetDriverByName('GTiff')
76 |
77 | outRaster = driver.Create(str(outfile), cols, rows, 1, typ)
78 | outRaster.SetGeoTransform((originX, pixelWidth, 0, originY, 0, -pixelHeight))
79 |
80 | outband = outRaster.GetRasterBand(1)
81 | if array.dtype == np.dtype(np.int32):
82 | # set noData value for integer rasters, for float rasters the input array is expected to have NaN values for NoData
83 | outband.SetNoDataValue(-999)
84 | outband.WriteArray(array)
85 |
86 | outRasterSRS = osr.SpatialReference()
87 | outRasterSRS.ImportFromEPSG(4326)
88 | outRaster.SetProjection(outRasterSRS.ExportToWkt())
89 |
90 | outband.FlushCache()
91 |
92 | if __name__ == "__main__":
93 |
94 | # commandline interface
95 |
96 | try:
97 | parser = argparse.ArgumentParser(
98 | description="""
99 | Converts a REGNIE file to a raster file (geotiff)
100 |
101 | This script can handle daily, monthly and multiannual REGNIE files
102 |
103 | Monthly and multiannual REGNIE files contain values in the unit mm and are converted to Int32 rasters with a NoData value of -999
104 | Daily REGNIE files contain values in the unit 1/10 mm, which are first converted to mm and then converted to Float64 rasters with a default NoData value
105 |
106 | REGNIE description: https://www.dwd.de/DE/leistungen/regnie/regnie.html
107 |
108 | REGNIE files are freely available from the DWD open data platform:
109 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/daily/regnie/
110 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/monthly/regnie/
111 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/multi_annual/regnie/
112 | """
113 | )
114 |
115 | parser.add_argument("regnie_file", type=str, help="input REGNIE file")
116 | parser.add_argument("output_file", type=str, help="output raster file (tif)")
117 |
118 | args = parser.parse_args()
119 |
120 | regnie2raster(Path(args.regnie_file), Path(args.output_file))
121 |
122 | print("Conversion successful!")
123 |
124 | except Exception as e:
125 |
126 | print(f"ERROR: {e}")
127 |
128 |
--------------------------------------------------------------------------------
/classes/ASCIIGridWriter.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pathlib import Path
3 | import numpy as np
4 |
5 | from .NumpyRadolanReader import NumpyRadolanReader
6 |
7 | default_nodata_value = -1.0
8 |
9 |
10 | class ASCIIGridWriter:
11 | """ASCIIGridWriter
12 |
13 | Creates a ESRI ASCII GRID (to convert this to GeoTIFF afterwards)
14 | from original binary RADOLAN file
15 |
16 | Created on 06.12.2020
17 | @author: Weatherman"""
18 |
19 | def __init__(self, np_2Ddata, precision, asc_filename_path, nodata_value=default_nodata_value):
20 | """
21 | :param np_2Ddata:
22 | :param precision:
23 | :param asc_filename_path:
24 | :param nodata_value: possibility to specify another missing value,
25 | e.g. for dBZ products (e.g. -50.0; -32.5 = neg. maximum)
26 | """
27 | print(self)
28 |
29 | self._np_2Ddata = np_2Ddata
30 | self._asc_filename_path = asc_filename_path
31 | self._prec = precision # precision: 1.0, 0.1, 0.01
32 |
33 | # catch 'None':
34 | if nodata_value is None:
35 | nodata_value = default_nodata_value
36 |
37 | self._nodata_value = nodata_value
38 |
39 | if nodata_value != default_nodata_value:
40 | self.out(f"nodata_value: {nodata_value}")
41 |
42 | def __str__(self):
43 | return self.__class__.__name__
44 |
45 | def out(self, s, ok=True):
46 | if ok:
47 | print(f"{self}: {s}")
48 | else:
49 | print(f"{self}: {s}", file=sys.stderr)
50 |
51 | def write(self):
52 | """
53 | Kapselt die verschiedenen internen Methoden (ganze RADOLAN-
54 | Datenverarbeitung als Schnittstelle nach außen.
55 | """
56 |
57 | nrows, ncols = self._np_2Ddata.shape
58 |
59 | d_projected_meters = {
60 | # key: row, value: tuple of projected meters: x0, y0
61 | 1500: (-673462, -5008645), # central europe composite, 1500x1400
62 | 1200: (-543197, -4822589), # WN, HG, 1200x1100 *)
63 | 1100: (-443462, -4758645), # extended national composite / RADKLIM: 1100x900
64 | 900: (-523462, -4658645) # national composite: 900x900
65 | }
66 | # *) taken y0 from HG format description and minus 1200 kilometers.
67 | # They specify the _upper_ left corner there.
68 |
69 | try:
70 | llcorner = d_projected_meters[nrows]
71 | except KeyError:
72 | raise NotImplementedError
73 |
74 | l_gis_header_template = []
75 | l_gis_header_template.append(f"ncols {ncols}")
76 | l_gis_header_template.append(f"nrows {nrows}")
77 | l_gis_header_template.append(f"xllcorner {llcorner[0]}")
78 | l_gis_header_template.append(f"yllcorner {llcorner[1]}")
79 | l_gis_header_template.append("cellsize 1000")
80 | l_gis_header_template.append(f"nodata_value {self._nodata_value}")
81 |
82 | gis_header = "\n".join(l_gis_header_template)
83 |
84 | # precision: 1.0, 0.1, 0.01
85 | if self._prec == 0.1:
86 | fmt = '%.1f'
87 | elif self._prec == 0.01:
88 | fmt = '%.2f'
89 | else:
90 | fmt = '%d' # as integer
91 |
92 | np_data = np.copy(self._np_2Ddata) # otherwise values will be changed
93 |
94 | #mask = np.isnan(np_data)
95 | #np_data[mask] = NODATA
96 | # -> ok
97 | np_data[np.isnan(np_data)] = self._nodata_value # set np.nan to ASCII value
98 |
99 | # np.flipud(): Flip array in the up/down direction.
100 | np.savetxt(self._asc_filename_path, np.flipud(np_data), fmt=fmt,
101 | delimiter=' ', newline='\n', header=gis_header, comments='') # footer=''
102 |
103 | self.out(f"write(): -> {self._asc_filename_path}")
104 |
105 | """
106 | if np.amax(data_clutter) > 0.0:
107 | save_file = path.join(self._temp_dir, out_file_bn + "_clutter.asc")
108 | self._write_ascii_grid(data_clutter, gis_header, save_file)
109 |
110 | if np.amax(data_ground) > 0.0:
111 | save_file = path.join(self._temp_dir, out_file_bn + "_ground.asc")
112 | self._write_ascii_grid(data_ground, gis_header, save_file)
113 |
114 | if np.amax(data_nodata) > 0.0:
115 | save_file = path.join(self._temp_dir, out_file_bn + "_nodata.asc")
116 | self._write_ascii_grid(data_nodata, gis_header, save_file)
117 | """
118 |
119 |
120 | def test_hg():
121 | print("*** Test HG ***\n")
122 |
123 | hg_path = Path("/run/media/loki/ungesichert/Testdaten/HG")
124 |
125 | for hg in hg_path.glob("HG*_???"):
126 | _to_asc(hg)
127 |
128 | def test_wn():
129 | print("*** Test WN ***\n")
130 | wn_path = Path("/run/media/loki/ungesichert/Testdaten/WN")
131 | wn1 = wn_path / "20221224-2200/WN2212242200_000"
132 | wn2 = wn_path / "20221226-1300/WN2212261300_000"
133 |
134 | for wn in (wn1, wn2):
135 | _to_asc(wn, -50.0)
136 |
137 | def _to_asc(bin, nodata_value=None):
138 | nrr = NumpyRadolanReader(bin) # FileNotFoundError
139 | nrr.read()
140 |
141 | asc_file = Path(bin.parent, bin.name + ".asc")
142 |
143 | ascii_writer = ASCIIGridWriter(nrr.data, nrr.precision, asc_file, nodata_value)
144 | ascii_writer.write()
145 |
146 | if __name__ == "__main__":
147 | """ Test ASCII convert """
148 |
149 | #test_hg()
150 | test_wn()
151 |
--------------------------------------------------------------------------------
/classes/Regnie.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | """
3 | Class for reading REGNIE files
4 |
5 | Can handle daily, monthly and multiannual REGNIE files
6 |
7 | REGNIE description: https://www.dwd.de/DE/leistungen/regnie/regnie.html
8 |
9 | REGNIE files are freely available from the DWD open data platform:
10 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/daily/regnie/
11 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/monthly/regnie/
12 | https://opendata.dwd.de/climate_environment/CDC/grids_germany/multi_annual/regnie/
13 |
14 | @author: Felix Froehlich
15 |
16 | partially based on Regnie2CSV.py by Mario Hafer, Deutscher Wetterdienst (DWD)
17 | """
18 | import re
19 | import gzip
20 | from pathlib import Path
21 |
22 | import numpy as np
23 |
24 | # REGNIE extent:
25 | Y_MAX = 971
26 | X_MAX = 611
27 |
28 | class Regnie:
29 |
30 | def __init__(self, file_regnie: Path):
31 | """
32 | Instantiates a new Regnie class instance representing a REGNIE file.
33 | The file is read immediately.
34 |
35 | :param file_regnie: path to the REGNIE file (can be gz compressed)
36 | """
37 | if not file_regnie.exists():
38 | raise FileNotFoundError(f"File not found: {file_regnie}")
39 |
40 | self._file = file_regnie
41 | self._datatype = self._get_datatype()
42 | self._data = None
43 |
44 | self._read_file()
45 |
46 | def _read_file(self):
47 | """
48 | Reads the REGNIE file and stores the values in the `data` property
49 | """
50 | if self._file.name.endswith(".gz"):
51 | file = gzip.open(self._file, "rt")
52 | else:
53 | file = open(self._file, "r")
54 |
55 | rows = []
56 | for i, line in enumerate(file, start=1):
57 | if i > Y_MAX:
58 | # in daily datasets, the last line contains other stuff we don't need
59 | break
60 | line = line.strip()
61 | # split line into strings of 4 characters each
62 | strings = []
63 | for j in range(len(line) // 4):
64 | strings.append(line[j*4:(j*4)+4])
65 |
66 | # convert strings to values depending on datatype
67 | if self.datatype in ["monthly", "multiannual"]:
68 | values = [int(s) for s in strings]
69 | elif self.datatype == "daily":
70 | values = [float(s) if s != "-999" else np.nan for s in strings]
71 | values = [v / 10.0 for v in values]
72 |
73 | rows.append(values)
74 |
75 | file.close()
76 |
77 | self._data = np.array(rows)
78 |
79 | def _get_datatype(self) -> str:
80 | """
81 | Determines the data type of a REGNIE file from its filename
82 |
83 | Examples:
84 | ra210627 - daily values
85 | RASA2110 - monthly values
86 | RAS9120.JAH - multiannual values
87 |
88 | :param file_regnie: REGNIE file
89 | :return: "daily", "monthly" or "multiannual"
90 | """
91 | if re.fullmatch(r"ra\d{6}(\.gz)?", self._file.name, re.I):
92 | return "daily"
93 | elif re.fullmatch(r"RASA\d{4}(\.gz)?", self._file.name, re.I):
94 | return "monthly"
95 | elif re.fullmatch(r"RAS\d{4}\.[a-z]+(\.gz)?", self._file.name, re.I):
96 | return "multiannual"
97 | else:
98 | raise Exception(f"Unable to determine REGNIE data type from filename {self._file.name}!")
99 |
100 | def _pixel_to_latlon(self, x: int, y: int) -> tuple:
101 | """
102 | Converts a REGNIE x,y pixel coordinate to a lat,lon coordinate
103 |
104 | :param x: x coordinate
105 | :param y: y coordinate
106 | :return: (lat, lon)
107 | """
108 | x_delta = 1.0 / 60.0
109 | y_delta = 1.0 / 120.0
110 |
111 | x_min = 6.0 - 10.0 * x_delta
112 | y_max = 55.0 + 10.0 * y_delta
113 |
114 | lon = x_min + (x - 1) * x_delta
115 | lat = y_max - (y - 1) * y_delta
116 |
117 | return lat, lon
118 |
119 | @property
120 | def data(self) -> np.array:
121 | """
122 | Returns the values contained in the REGNIE file as a numpy array
123 |
124 | The data type of the array is integer for monthly and multiannual values and float for daily values
125 | REGNIE error values ("-999") are preserved as -999 for monthly and multiannual values and replaced with np.nan for daily values
126 | Daily REGNIE values are converted from 1/10 mm to mm
127 |
128 | :return: a two-dimensional numpy array
129 | """
130 | return self._data
131 |
132 | @property
133 | def datatype(self) -> str:
134 | """
135 | Returns the data type of the REGNIE file
136 |
137 | :return: either "daily", "monthly" or "multiannual"
138 | """
139 | return self._datatype
140 |
141 | @property
142 | def statistics(self) -> dict:
143 | """
144 | Returns statistics from the REGNIE data
145 |
146 | :return: dictionary of statistical values
147 | """
148 | # extract only valid cells
149 | if self.datatype == "daily":
150 | valid_data = np.extract(~np.isnan(self.data), self.data)
151 | elif self.datatype in ["monthly", "multiannual"]:
152 | valid_data = np.extract(self.data != -999, self.data)
153 |
154 | stats = {
155 | "total_pixels": self.data.size,
156 | "valid_pixels": valid_data.size,
157 | "non_valid_pixels": self.data.size - valid_data.size,
158 | "max": np.max(valid_data),
159 | "min": np.min(valid_data),
160 | "mean": np.mean(valid_data)
161 | }
162 |
163 | return stats
164 |
--------------------------------------------------------------------------------
/symbology/WN.qml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 1
6 | 1
7 | 0
8 |
9 |
10 |
11 | 2022-12-24T21:56:00Z
12 | 2022-12-24T22:00:00Z
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | None
38 | WholeRaster
39 | Estimated
40 | 0.02
41 | 0.98
42 | 2
43 |
44 |
45 |
46 |
47 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | resamplingFilter
97 |
98 | 0
99 |
100 |
--------------------------------------------------------------------------------
/classes/SettingsTab.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 |
4 | from qgis.core import QgsProject
5 | from qgis.PyQt.QtWidgets import QFileDialog
6 |
7 | # own classes:
8 | from .ActionTabBase import ActionTabBase # base class
9 |
10 |
11 |
12 | class SettingsTab(ActionTabBase):
13 | """
14 | SettingsTab
15 |
16 | Separate __actions__ of setting operations from the other components
17 |
18 | Created on 20.12.2020
19 | @author: Weatherman
20 | """
21 |
22 |
23 | def __init__(self, iface, model, dock):
24 | super().__init__(iface, model, dock)
25 |
26 | self.dock.btn_select_storage_dir.clicked.connect(self._select_storage_dir)
27 | self.dock.btn_save.clicked.connect(self._write_new_storage_location)
28 |
29 | """
30 | fill projections
31 | Qt-element is connected with the projection list, via same index
32 | """
33 |
34 | for number, proj_desc in model.dict_projections.items():
35 | if number > 999: # 4 digits expected
36 | entry = proj_desc
37 | else: # !: list type expected: desc, proj4-string
38 | assert type(proj_desc) is list
39 | entry, _ = proj_desc
40 |
41 | self.dock.cbbox_projections.addItem(entry)
42 | # for
43 |
44 |
45 |
46 | """
47 | Actions
48 | """
49 |
50 | def _select_storage_dir(self):
51 | dock = self.dock # shorten
52 |
53 | start_dir = dock.textfield_path.text()
54 | # We assume a set up file here:
55 | if not start_dir:
56 | start_dir = str(Path.home())
57 |
58 | # parent, caption, directory, file_filter
59 | selected_dir = QFileDialog.getExistingDirectory(dock, 'Select RADOLAN/RADKLIM directory', start_dir,
60 | # these additional parameters are used, because QFileDialog otherwise doesn't start with the given path:
61 | QFileDialog.DontUseNativeDialog)
62 |
63 | if not selected_dir:
64 | return # preserve evtl. filled line
65 |
66 | # NOT 'radolan2map/radolan2map'!
67 | const_last_part = 'radolan2map'
68 | if not selected_dir.endswith(const_last_part):
69 | complete_path = Path(selected_dir) / const_last_part
70 | else:
71 | complete_path = selected_dir
72 |
73 | # Set:
74 | self.storage_dir = complete_path
75 |
76 | # after folder selection enable save button:
77 | dock.btn_save.setEnabled(True)
78 |
79 |
80 |
81 | def update_projection_based_on_current_project(self):
82 | """
83 | - Determines projection (EPSG code) of current project
84 | - insert it in the list of projections
85 | - and choose it. """
86 |
87 | self.out("update_projection_based_on_current_project()")
88 |
89 | if not QgsProject.instance().fileName(): # if project loaded:
90 | return
91 |
92 | epsg_code = self._iface.mapCanvas().mapSettings().destinationCrs().authid()
93 | # Problem occured on Windows 10 with QGIS 3.10: empty 'epsg_code'.
94 | # Maybe with the application of "setDestinationCrs()" in 'create_default_project()' the problem isn't existent anymore.
95 | if not epsg_code:
96 | self.out("project CRS couldn't be determined; this is a unknown problem on Windows - not critical",
97 | False)
98 | return
99 |
100 | def add_projection(projection_description, epsg_code):
101 | """ add projection to ComboBox and a list with epsg codes,
102 | which corresponds each other. """
103 | self.dock.cbbox_projections.addItem(projection_description)
104 | self._model.projections.append(epsg_code)
105 |
106 | # add only, if this projection doesn't exist:
107 | if not epsg_code in self._model.projections:
108 | projection_description = f"Project: {epsg_code}"
109 | add_projection(projection_description, epsg_code)
110 | # projection appended last, select last entry:
111 | index = len(self._model.projections) - 1 # -1 as last index doesn't work!
112 | self.dock.cbbox_projections.setCurrentIndex(index)
113 |
114 |
115 | def _write_new_storage_location(self):
116 | """ triggered by save button """
117 |
118 | path_to_save = self.storage_dir # this dir probably doesn't exist, so we can't check it, we need the parent
119 | parent_dir = path_to_save.resolve().parent
120 |
121 | # protection: check if writetable:
122 | if not os.access(parent_dir, os.W_OK):
123 | l_msg = []
124 | l_msg.append(f'Directory "{parent_dir}" is not writable!')
125 | # User hint for Windows:
126 | if str(parent_dir).startswith('C:'):
127 | l_msg.append("(as a user, you probably do not have write permissions directly on C:)")
128 | l_msg.append('\nPlease choose another one.')
129 | msg = '\n'.join(l_msg)
130 | super()._show_critical_message_box(msg, 'Write permission error')
131 | return
132 |
133 | data_root_def_file = self._model.data_root_def_file
134 |
135 | with data_root_def_file.open('w') as f:
136 | f.write(str(path_to_save))
137 |
138 | self.out("_write_new_storage_location()")
139 | print(f" saved path '{path_to_save}' to file '{data_root_def_file}'")
140 |
141 | dock = self.dock # shorten
142 | dock.btn_save.setEnabled(False) # show user, that action was performed
143 | # update new data path:
144 | self._model.data_root = Path(path_to_save)
145 |
146 | dock.tabWidget.setTabEnabled(dock.TAB_RADOLAN_LOADER, True)
147 | dock.tabWidget.setTabEnabled(dock.TAB_RADOLAN_ADDER, True)
148 | dock.tabWidget.setTabEnabled(dock.TAB_REGNIE, True)
149 |
150 | # ..........................................................
151 |
152 | @property
153 | def storage_dir(self):
154 | return Path(self.dock.textfield_path.text())
155 | @storage_dir.setter
156 | def storage_dir(self, d):
157 | self.dock.textfield_path.setText(str(d))
158 |
159 |
--------------------------------------------------------------------------------
/classes/ActionTabRegnie.py:
--------------------------------------------------------------------------------
1 | """
2 | ActionTabRegnie
3 |
4 | Separate __actions__ of REGNIE operations from the other components
5 |
6 | Created on 01.12.2020
7 | @author: Weatherman
8 | """
9 | from pathlib import Path
10 |
11 | from qgis.PyQt.QtWidgets import QFileDialog
12 |
13 | # own classes:
14 | from .ActionTabBase import ActionTabBase # base class
15 | from .LayerLoader import LayerLoader
16 | from .Regnie import Regnie
17 | from . import regnie2raster as r2r
18 |
19 | class ActionTabRegnie(ActionTabBase):
20 | '''
21 | classdocs
22 | '''
23 |
24 |
25 | def __init__(self, iface, model, dock):
26 | '''
27 | Constructor
28 | '''
29 |
30 | super().__init__(iface, model, dock)
31 |
32 |
33 | self.dock.btn_select_regnie.clicked.connect(self._select_regnie_file)
34 | self.dock.btn_load_regnie.clicked.connect(self._run)
35 |
36 |
37 |
38 |
39 | """
40 | Actions
41 | """
42 |
43 | def _select_regnie_file(self):
44 | text = self.regnie_file
45 | start_dir = Path(text).parent if text else Path.home()
46 |
47 | # after title string: start path, file_filter
48 | regnie_file, _ = QFileDialog.getOpenFileName(self.dock, "Please select a REGNIE file",
49 | str(start_dir), None,
50 | # these additional parameters are used, because QFileDialog otherwise doesn't start with the given path:
51 | None, QFileDialog.DontUseNativeDialog)
52 | if not regnie_file:
53 | return # keep path anyway
54 |
55 | self.regnie_file = regnie_file # -> enable button
56 |
57 |
58 |
59 |
60 | def _run(self):
61 | self.out("_run()")
62 |
63 | # shorten:
64 | regnie_file = self.regnie_file
65 | dock = self.dock
66 |
67 | # field was leaved empty
68 | if not regnie_file:
69 | super()._show_critical_message_box("REGNIE input file wasn't specified!")
70 | dock.btn_load_regnie.setEnabled(False)
71 | return
72 |
73 |
74 | regnie_file = Path(regnie_file)
75 |
76 | if not regnie_file.exists():
77 | super()._show_critical_message_box(f"File\n'{regnie_file}'\nnot found!")
78 | dock.btn_load_regnie.setEnabled(False)
79 | return
80 |
81 |
82 | self._model.set_data_dir('regnie')
83 | data_dir = self._model.data_dir
84 |
85 | try: # include everything that can raise exceptions
86 | try:
87 | self._model.create_storage_folder_structure()
88 | self.out(f"create data dir for converted REGNIE: '{data_dir}'")
89 | except FileExistsError:
90 | pass
91 |
92 | # instantiate a Regnie class instance
93 | rg = Regnie(regnie_file)
94 | self.out(f"Detected REGNIE datatype: {rg.datatype}")
95 |
96 | # convert to raster
97 | self.out("Starting REGNIE to raster conversion...")
98 | regnie_raster_file = data_dir / f"{regnie_file.name.replace('.gz', '')}.tif"
99 | try:
100 | #TODO: this currently reads the REGNIE file a second time
101 | r2r.regnie2raster(regnie_file, regnie_raster_file)
102 | except ModuleNotFoundError as e:
103 | """ Under Linux a "ModuleNotFoundError: No module named '_gdal'"
104 | error can occur. """
105 | msg = f"Exception: {e}:\nPossibly a GDAL installation error on Linux(?)"
106 | self.out(f"{msg}")
107 | super()._show_critical_message_box(msg, 'REGNIE raster conversion error')
108 | return
109 | except Exception as e:
110 | msg = f"Exception: {e}"
111 | self.out(f"{msg}")
112 | super()._show_critical_message_box(msg, 'REGNIE raster conversion error')
113 | return
114 |
115 | self.out(f"Successfully created REGNIE raster file {regnie_raster_file}!")
116 |
117 | except Exception as e:
118 | msg = f"{e}, wrong format!"
119 | super()._show_critical_message_box(msg, 'Layer loading error') # disable here, because it was only set a folder
120 | dock.btn_load_regnie.setEnabled(False)
121 | # Reset - don't save the wrong data for the next run!:
122 | #dock.text_regnie.clear()
123 | #self.regnie_file = str(regnie_file.parent) # nevertheless save last path for next suggestion
124 | return
125 |
126 |
127 | """
128 | Add the layers ...
129 | """
130 |
131 | # Determine prepared QML-File delivered with the plugin:
132 | d_regnie_qml = {
133 | 'daily': "regnie_raster_daily.qml",
134 | 'monthly': "regnie_raster_monthly.qml",
135 | 'multiannual': "regnie_raster_yearly.qml",
136 | }
137 |
138 | ll = LayerLoader(self._iface) # 'iface' is from 'radolan2map'
139 |
140 | # load the regnie raster file
141 | qml_file = self._model.symbology_path / d_regnie_qml[rg.datatype]
142 | ll.load_raster(regnie_raster_file, qml_file)
143 |
144 | # fill statistics output
145 | stats = rg.statistics
146 | dock.text_filename.setText(regnie_file.name.replace(".gz", ""))
147 | dock.text_shape.setText(f"{rg.data.shape}")
148 | dock.text_max.setText(f"{stats['max']:.1f}")
149 | dock.text_min.setText(f"{stats['min']:.1f}")
150 | dock.text_mean.setText(f"{stats['mean']:.1f}")
151 | dock.text_total_pixels.setText(f"{stats['total_pixels']}")
152 | dock.text_valid_pixels.setText(f"{stats['valid_pixels']}")
153 | dock.text_nonvalid_pixels.setText(f"{stats['non_valid_pixels']}")
154 |
155 | super()._enable_and_show_statistics_tab()
156 | super()._finish()
157 |
158 | # ......................................................................
159 |
160 |
161 | # for saving:
162 |
163 | @property
164 | def regnie_file(self):
165 | return self.dock.text_regnie.text()
166 | @regnie_file.setter
167 | def regnie_file(self, f):
168 | self.dock.text_regnie.setText(f)
169 | # after folder selection enable load button:
170 | self.dock.btn_load_regnie.setEnabled(True)
171 |
172 |
173 |
--------------------------------------------------------------------------------
/metadata.txt:
--------------------------------------------------------------------------------
1 | # This file contains metadata for your plugin.
2 | # This file should be included when you package your plugin.
3 | # Mandatory items:
4 |
5 | [general]
6 | name=radolan2map
7 | qgisMinimumVersion=3.0
8 | # short text which describes the plugin, no HTML allowed:
9 | description=Brings DWD precipitation products like RADOLAN, RADKLIM and REGNIE onto a map
10 | version=1.8
11 | author=Weatherman
12 | email=radolan2map@e.mail.de
13 |
14 | # more detailed description:
15 | about=This QGIS plugin brings precipitation radar data from German Meteorological Service (DWD, https://www.dwd.de) in RADOLAN/RADKLIM binary format in any grid dimension as well as REGNIE on a map
16 | You also can **add** up RADOLAN data!
17 | Applications: hydrometeorological and agricultural analyses.
18 |
19 | RADOLAN overview: https://dwd.de/RADOLAN
20 | RADOLAN data (recent): https://opendata.dwd.de/climate_environment/CDC/grids_germany/hourly/radolan/recent/bin
21 | RADKLIM data (radar climatology): https://opendata.dwd.de/climate_environment/CDC/grids_germany/hourly/radolan/reproc/2017_002/bin
22 | Radar products on DWD Open Data: https://opendata.dwd.de/weather/radar/composit/
23 |
24 | A RADOLAN-RW-testfile (gzipped binary) is included in directory 'example/sample_file/'.
25 |
26 | The plugin was successfully tested with
27 | - QGIS 3.18 on Windows 10
28 | - QGIS 3.36 Linux openSUSE Tumbleweed
29 |
30 | Please restart QGIS after updating the plugin!
31 |
32 | Code Repository: https://github.com/Weathermann/radolan2map
33 |
34 | More info about installation/usage: https://gitlab.com/Weatherman_/radolan2map/wikis/home
35 |
36 |
37 | tracker=https://github.com/Weathermann/radolan2map/issues
38 | repository=https://github.com/Weathermann/radolan2map
39 | # End of mandatory metadata
40 |
41 | # Recommended items:
42 |
43 | hasProcessingProvider=no
44 | # Uncomment the following line and add your changelog:
45 | # string, can be multiline, no HTML allowed
46 | changelog=
47 | 1.8, 2024-04-21:
48 | * Functionality
49 | - allow multiselect in RADOLAN load tab
50 | - automatically layer grouping (day), collapsed
51 | - reduced log output for performance reason
52 | * fix
53 | - temporal settings (after project reload; UTC error?)
54 | 1.7, 2023-01-23:
55 | * Functionality - new products: display of the
56 | - HG product (precipitation type): https://opendata.dwd.de/weather/radar/composit/hg/
57 | - WN product (reflectivity product): https://opendata.dwd.de/weather/radar/composit/wn/
58 | - bzip2 format is now readable directly ('HG'), not 'tar.bz2' ('WN')
59 | * Projection
60 | - ASCIIGridWriter: added projection for WN (lower left corner)
61 | - added projection for stereographic projection based on WGS ellipsoid (POLARA products)
62 | * correction: german radar network: radius 150 km
63 | 1.6, 2021-12-04:
64 | - REGNIE files are now converted to GeoTIFF rasters instead of CSV
65 | 1.5, 2021-02-26: Bugfix version
66 | - ### bug fixed that prevents defining the data dir ###
67 | - fixed a possible bug at RADOLAN adding (handling of NaN values)
68 | - optimization: RADOLAN adder file access: no glob for every file any more
69 | - display of new features, which is presented to the user once
70 | 1.4, 2020-12-26: Christmas gift: "radolan summation version"
71 | - in a separate tab you can now generate a sum product based on individual RADOLAN files!
72 | - revised handling of 'interval unit' to assign the proper QML color scale
73 | - redesign of internal processes (tasks per tab)
74 | - the last used processing tab is saved (will be shown on the next run)
75 | - you can define your own logo in 'config.ini' for usage in print layout
76 | 1.3, 2020-05-21: "print layout version"
77 | - print layout implemented
78 | - catch global exception, show it to the user as a window
79 | - catch exception at reading corrupted RADOLAN bin file and show this in GUI window
80 | - links to REGNIE download site included
81 | - layer storage folder reorganized - root and data dir
82 | - separate subdirectories for the different data types: 'regnie', 'radolan', 'radklim'
83 | 1.2, 2020-01-24: "REGNIE version"
84 | - from now 'radolan2map' can load REGNIE raster files (daily, monthly, yearly)
85 | but the support is rudimentary (data is displayed as point layer)
86 | - new button which loads DWDs radar network from layer definition file (qlr)
87 | 1.1, 2019-12-10:
88 | - current DWD radar network in template project included
89 | - combo box for selection of destination projection
90 | - settings are saved
91 | - Checks: writable check of user defined storage folder and mask file
92 | - data download link included, icons for links
93 | 1.0, 2019-11-17: "Tab-Version"
94 | - with this extension it is time for a final version now
95 | - tab: statistics about the processed product
96 | - product history: stores history of selected products
97 | 0.7, 2019-10-30: "toolbar version"
98 | - Toolbar icons
99 | - switch to alternative Qt-dialog, because of non-working start directory
100 | - test product on the first 2 characters to determine if it is a "X-product"
101 | so it is independent from filename
102 | 0.6, 2019-10-09:
103 | - Bugfix: select mask button
104 | - 'data dir def file' is stored in QGIS profile folder now, so that it is not lost by plugin update
105 | 0.5, 2019-10-07: layer storage version
106 | - request to the user where the data directory should be set up
107 | - cut GeoTIFF directly using shapefile
108 | - GUI extension: conversion of RVP6 units -> mm / 5min
109 | - Optimization of the GUI elements
110 | 0.4, 2019-09-27: RX version
111 | - RX processing, also WX, EX
112 | - hopefully improved floating QDockWidget
113 | - template project with state capitals of Germany (as GeoPackage now)
114 | - revised symbology QMLs
115 | 0.3, 2019-09-19: new function: 'exclude zeroes', fixed clipping problem (Windows), using "DEU_adm0" as border dataset now
116 | 0.2, 2019-09-15: changed from QDialog to QDockWidget
117 | 0.1, 2019-09-02: first release, plugin uses QDialog, experimental state
118 |
119 | # Tags are comma separated with spaces allowed
120 | tags=DWD,radar,RADKLIM,RADOLAN,REGNIE,raster,precipitation,QPE
121 |
122 | homepage=https://gitlab.com/Weatherman_/radolan2map/wikis/home
123 | # Category of the plugin: Raster, Vector, Database or Web
124 | category=Raster
125 | icon=img/icon.png
126 | # experimental flag
127 | experimental=False
128 |
129 | # deprecated flag (applies to the whole plugin, not just a single version)
130 | deprecated=False
131 |
132 | # Since QGIS 3.8, a comma separated list of plugins to be installed
133 | # (or upgraded) can be specified.
134 | # Check the documentation for more information.
135 | # plugin_dependencies=
136 |
137 | # If the plugin can run on QGIS Server.
138 | server=False
139 |
140 |
--------------------------------------------------------------------------------
/classes/def_products.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # String: String
4 | # product id, product description
5 | dict_titles = {
6 |
7 | # RADOLAN (online) base products:
8 | 'EY': "Europäische quantitative Radardaten, qualitätskorrigiert",
9 | 'EZ': "Europäische quantitative Radardaten, nicht qualitätskorrigiert",
10 | 'RH': "(qualitätsgeprüfte) Radardaten nach Abschattungskorrektur und nach Anwendung der verfeinerten Z-R-Beziehung in Niederschlagshöhen umgerechnet und auf eine Stunde aufsummiert",
11 | 'RB': "Radardaten, vorangeeicht mit einem Faktor, Stundensumme",
12 | 'RW': "Radardaten nach Aneichung mit der gewichteten Mittelung aus zwei Standardverfahren, Stundensumme",
13 | 'RO': "Radardaten nach Anwendung der Standard-Z-R-Beziehung in Niederschlagshöhen umgerechnet",
14 | 'RY': "Qualitätsgeprüfte Radardaten nach Abschattungskorrektur und nach Anwendung der verfeinerten Z-R-Beziehung in Niederschlagshöhen umgerechnet",
15 | 'RZ': "Radardaten nach Abschattungskorrektur und nach Anwendung der verfeinerten Z-R-Beziehung in Niederschlagshöhen umgerechnet",
16 |
17 | # hourly/daily sums:
18 | 'S2': "2h-Niederschlagssumme",
19 | 'S3': "3h-Niederschlagssumme",
20 | 'SH': "12h-Niederschlagssumme",
21 | 'SF': "24h-Niederschlagssumme",
22 | 'D2': "48h-Niederschlagssumme",
23 | 'D3': "72h-Niederschlagssumme",
24 |
25 | "DC": "Anzahl Stunden (h) am Tag mit Clutter-Kennung",
26 | "DF": "Anzahl Stunden (h) am Tag mit Fehlwert",
27 | "DI": "Tagesniederschlagssumme aus REGNIE",
28 | "DK": u"Tagesniederschlagssumme aus RW. Fehlwerte werden, wenn verfügbar, mit Werten aus REGNIE aufgefüllt.",
29 | "DM": "Max. 1h-Niederschlagssumme des Tages",
30 | "DR": u"Anzahl Stunden (h) am Tag mit Niederschlag ausschließlich aus Bodenmessung",
31 | "DS": "Anzahl Stunden (h) am Tag mit Niederschlag ≥ 0.1 mm",
32 | "DU": u"Anzahl Stunden (h) am Tag an denen der Bemessungsniederschlag (T > 1a) überschritten wird",
33 | "DW": "Tagesniederschlagssumme",
34 | "GA": "Skalierte Gesamtzeitraumsniederschlagssumme",
35 | "GC": "Anzahl Stunden (h) im Gesamtzeitraum mit Clutter-Kennung",
36 | "GF": "Anzahl Stunden (h) im Gesamtzeitraum mit Fehlwert",
37 | "GI": "Gesamtzeitraumsniederschlagssumme aus REGNIE",
38 | "GK": u"Gesamtzeitraumsniederschlagssumme aus DW. Fehlwerte werden, wenn verfügbar, mit Werten aus REGNIE aufgefüllt.",
39 | "GM": "Max. 1h-Niederschlagssumme des Gesamtzeitraums",
40 | "GS": "Anzahl Stunden (h) im Gesamtzeitraum mit Niederschlag ≥ 0.1 mm",
41 | "GU": u"Anzahl Stunden (h) im Gesamtzeitraum an denen der Bemessungsniederschlag (T > 1a) überschritten wird",
42 | "GW": "Gesamtzeitraumsniederschlagssumme",
43 | "HA": "Skalierte Halbjahresniederschlagssumme",
44 | "HC": "Anzahl Stunden (h) im Halbjahr mit Clutter-Kennung",
45 | "HF": "Anzahl Stunden (h) im Halbjahr mit Fehlwert",
46 | "HI": "Halbjahresniederschlagssumme aus REGNIE",
47 | "HK": u"Halbjahresniederschlagssumme aus DW. Fehlwerte werden, wenn verfügbar, mit Werten aus REGNIE aufgefüllt.",
48 | "HM": "Max. 1h-Niederschlagssumme der Jahreszeit",
49 | "HS": "Anzahl Stunden (h) im Halbjahr mit Niederschlag ≥ 0.1 mm",
50 | "HU": u"Anzahl Stunden (h) im Halbjahr an denen der Bemessungsniederschlag (T > 1a) überschritten wird",
51 | "HW": "Halbjahresniederschlagssumme",
52 | "JA": "Skalierte Jahresniederschlagssumme",
53 | "JC": "Anzahl Stunden (h) im Jahr mit Clutter-Kennung",
54 | "JF": "Anzahl Stunden (h) im Jahr mit Fehlwert",
55 | "JI": "Jahresniederschlagssumme aus REGNIE",
56 | "JK": u"Jahresniederschlagssumme aus DW. Fehlwerte werden, wenn verfügbar, mit Werten aus REGNIE aufgefüllt.",
57 | "JM": "Max. 1h-Niederschlagssumme des Jahres",
58 | "JS": "Anzahl Stunden (h) im Jahr mit Niederschlag ≥ 0.1 mm",
59 | "JU": u"Anzahl Stunden (h) im Jahr an denen der Bemessungsniederschlag (T > 1a) überschritten wird",
60 | "JW": "Jahresniederschlagssumme",
61 | "MA": "Skalierte Monatsniederschlagssumme",
62 | "MC": "Anzahl Stunden (h) im Monat mit Clutter-Kennung",
63 | "MF": "Anzahl Stunden (h) im Monat mit Fehlwert",
64 | "MI": "Monatsniederschlagssumme aus REGNIE",
65 | "MK": "Monatsniederschlagssumme aus DW. Fehlwerte werden, wenn verfügbar, mit Werten aus REGNIE aufgefüllt.",
66 | "MM": "Max. 1h-Niederschlagssumme des Monats",
67 | "MS": "Anzahl Stunden (h) im Monat mit Niederschlag ≥ 0.1 mm",
68 | "MU": u"Anzahl Stunden (h) im Monat an denen der Bemessungsniederschlag (T > 1a) überschritten wird",
69 | "MW": "Monatsniederschlagssumme",
70 | "QA": "Skalierte Jahreszeitenniederschlagssumme",
71 | "QC": "Anzahl Stunden (h) in der Jahreszeit mit Clutter-Kennung",
72 | "QF": "Anzahl Stunden (h) in der Jahreszeit mit Fehlwert",
73 | "QI": "Jahreszeitenniederschlagssumme aus REGNIE",
74 | "QK": u"Jahreszeitenniederschlagssumme aus DW. Fehlwerte werden, wenn verfügbar, mit Werten aus REGNIE aufgefüllt.",
75 | "QM": "Max. 1h-Niederschlagssumme der Jahreszeit",
76 | "QS": "Anzahl Stunden (h) in der Jahreszeit mit Niederschlag ≥ 0.1 mm",
77 | "QU": u"Anzahl Stunden (h) in der Jahreszeit an denen der Bemessungsniederschlag (T > 1a) überschritten wird",
78 | "QW": "Jahreszeitenniederschlagssumme",
79 | "SJ": "Niederschlagssumme seit Jahresbeginn",
80 | "SM": "Niederschlagssumme seit Monatsbeginn",
81 | "SQ": "6h-Niederschlagssumme",
82 | "SY": "Niederschlagssumme für das hydrologische Jahr (Beginn November)",
83 | "UA": u"Anzahl Stunden (h) am Tag mit Überschreitung der Starkregen-Warnstufe 2 (markantes Wetter)",
84 | "UB": u"Anzahl Stunden (h) am Tag mit Überschreitung der Starkregen-Warnstufe 3 (Unwetter)",
85 | "UC": u"Anzahl Stunden (h) am Tag mit Überschreitung der Starkregen-Warnstufe 4 (extremes Unwetter)",
86 | "UD": u"Anzahl Stunden (h) am Tag mit Überschreitung der Dauerregen-Warnstufe 2 (markantes Wetter)",
87 | "UE": u"Anzahl Stunden (h) am Tag mit Überschreitung der Dauerregen-Warnstufe 3 (Unwetter)",
88 | "UF": u"Anzahl Stunden (h) am Tag mit Überschreitung der Dauerregen-Warnstufe 4 (extremes Unwetter)",
89 | "W1": "7d-Niederschlagssumme basierend auf RW-Produkt",
90 | "W2": "14d-Niederschlagssumme basierend auf RW-Produkt",
91 | "W3": "21d-Niederschlagssumme basierend auf RW-Produkt",
92 | "W4": "30d-Niederschlagssumme basierend auf RW-Produkt",
93 | 'YW': "quasi-angeeichte 5-Minuten-Niederschlagsraten (RY nach Skalierung mit dem Quasianeichungsfaktor berechnet aus Verhältnis RW zu RH)",
94 |
95 | # X-products - in RVP6 units:
96 | 'RX': "Radardaten ohne Korrektur (in RVP6-Units)",
97 | 'WX': "Qualitätsgeprüfte Radardaten (in RVP6-Units)",
98 | 'EX': "Europäische Radardaten (in RVP6-Units)",
99 |
100 | # B = R-factor products:
101 | "BR": "Ereignisbasierter R-Faktor nach DIN19708 [kJm2*mm/h]",
102 | "BD": "Dauer des Ereignisses in Stunden ([h])",
103 | "BK": "Kinetische Energie des Ereignisses [kJ/m2]",
104 | "BI": "Maximale 30-Minuten-Intensität [mm/h]",
105 | "BJ": "Mittlerer Jahres-R-Faktor [kJm2*mm/h]",
106 | "BQ": "Mittlerer Jahreszeit-R-Faktor [kJm2*mm/h]",
107 | "BM": "Mittlerer Monats-R-Faktor [kJm2*mm/h]",
108 | "BY": "R-Faktor (Jahressumme) [kJm2*mm/h]",
109 |
110 | # Not RADOLAN:
111 | "HG": "Deutschlandkomposit der Niederschlagsart in 2m Höhe über Grund",
112 | "WN": "Deutschlandkomposit der Reflektivität",
113 | }
114 |
--------------------------------------------------------------------------------
/classes/ActionTabBase.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pathlib import Path
3 | import time
4 |
5 | from qgis.core import QgsProject, QgsPrintLayout, QgsReadWriteContext
6 | from qgis.PyQt.QtWidgets import QMessageBox
7 | # For the QGIS printing template:
8 | from qgis.PyQt.QtXml import QDomDocument
9 |
10 |
11 | class ActionTabBase:
12 | """ ActionTabBase
13 |
14 | Base class of all specialized tab classes
15 |
16 | ActionTab classes act as it's own controllers
17 |
18 | Created on 19.12.2020
19 | @author: Weatherman
20 | """
21 |
22 | def __init__(self, iface, model, dock):
23 | print(self)
24 |
25 | self._iface = iface
26 | self._model = model
27 | self.dock = dock
28 |
29 | def __str__(self):
30 | return self.__class__.__name__
31 |
32 | def out(self, s, ok=True):
33 | if ok:
34 | print(f"{self}: {s}")
35 | else:
36 | print(f"{self}: {s}", file=sys.stderr)
37 |
38 | def _show_critical_message_box(self, msg, caption='Exception catched'):
39 | self.out(msg, False)
40 | QMessageBox.critical(self._iface.mainWindow(), caption, msg)
41 |
42 | def _enable_and_show_statistics_tab(self): # TODO 11.03.2024: replace with method in 'gui.py'?
43 | self.dock.tabWidget.setTabEnabled(self.dock.TAB_STATISTICS, True)
44 | self.dock.tabWidget.setCurrentIndex(self.dock.TAB_STATISTICS) # show statistics tab
45 |
46 | def _check_create_project(self):
47 | """
48 | If no project file not loaded when running plugin
49 | """
50 | project = QgsProject.instance()
51 | project_fn = project.fileName()
52 | # for inspection of the project file, to determine, if it is a real file:
53 | # QGIS 3.22: seems to be always 'True' for exists() (?)
54 | #print(f"### project={project}, Path={project_fn}, exists={Path(project_fn).exists()}")
55 |
56 | # prepare project
57 | # QGIS 3.22: seems to be always 'True' for exists() (?), so check for content too:
58 | if project_fn and Path(project_fn).exists():
59 | return
60 |
61 | self.out("### no project open -> create a new one ###")
62 | project = self._model.create_default_project()
63 |
64 | yyyymmddHHMM_with_ext = time.strftime("%Y%m%d_%H%M") + '.qgs'
65 | new_file = self._model.data_root / yyyymmddHHMM_with_ext
66 | self.out(f"write: {new_file}")
67 | project.write(str(new_file))
68 |
69 | def _finish(self):
70 | """ prints a unified message at end of operation """
71 | self.out("*** Whole process finished! ***")
72 |
73 | def _load_print_layout(self, layer_name, prod_id, dt=None):
74 | window_title = "Print" # will be checked with a found composer title
75 |
76 | """
77 | Avoid creating a new PrintComposer again and again here.
78 | Otherwise more and more composers will be added to the list - even with the same name.
79 | """
80 |
81 | project = QgsProject.instance()
82 | # From it, you can get the current layoutManager instance and deduce the layouts
83 | layout_manager = project.layoutManager()
84 |
85 | layout = layout_manager.layoutByName(window_title)
86 |
87 | if not layout:
88 | self.out("no composer found; creating one...")
89 |
90 | # Load the template into the composer
91 | # QGIS 2:
92 | #active_composer = self.iface.createNewComposer(window_title) #createNewComposer()
93 | #active_composer = QgsComposition(QgsProject.instance())
94 |
95 | #layout = QgsLayout(project)
96 | layout = QgsPrintLayout(project)
97 | layout.initializeDefaults() # initializes default settings for blank print layout canvas
98 |
99 | q_xmldoc = self._create_qdocument_from_print_template_content()
100 |
101 | # load layout from template and add to Layout Manager
102 | #layout.composition().loadFromTemplate(q_xmldoc) # QGIS 2
103 | layout.loadFromTemplate(q_xmldoc, QgsReadWriteContext())
104 | layout.setName(window_title)
105 |
106 | layout_manager.addLayout(layout)
107 |
108 | # Update Logo:
109 |
110 | #logo_item = layout.getComposerItemById('logo') # QGIS 2
111 | logo_item = layout.itemById('logo')
112 | logo_image = self._model.logo_path
113 | self.out(f"Logo: {logo_image}")
114 | if logo_image.exists():
115 | logo_item.setPicturePath(str(logo_image))
116 | else:
117 | self.out(f" ERROR: logo '{logo_image}' not found!", False)
118 | # if
119 |
120 |
121 | """
122 | Hier versuche ich ein für die Überschrift mit einer ID ('headline') versehenes
123 | QgsLabel aus dem Template ausfindig zu machen. Ich mache das hier sehr kompliziert,
124 | es gibt bestimmt einen einfacheren Weg.
125 | Folgendes hat NICHT funktioniert:
126 | map_item = active_composer.getComposerItemById('headline')
127 | print(active_composer.items())
128 | liefert: [, , ... ]
129 | """
130 |
131 | ''' other possibility:
132 | for item in list(layout.items()):
133 | #if type(item) != QgsComposerLabel:
134 | '''
135 |
136 | # QgsComposerLabel:
137 | composer_label = layout.itemById('headline') # a QgsComposerLabel was provided with the ID 'headline' in the template
138 | # -> None if not found
139 |
140 | if composer_label:
141 | title = self._model.title(prod_id, dt)
142 | subtitle = ""
143 | if prod_id != 'RW':
144 | subtitle = f"\n{prod_id}-Produkt (Basis: RW)"
145 |
146 | composer_label.setText(title + subtitle)
147 | else:
148 | # A note that the template needs to be revised:
149 | self.out("no element with id 'headline' found!", False)
150 |
151 |
152 | legend = layout.itemById('legend')
153 |
154 | if not legend:
155 | self.out("legend couldn't created!", False)
156 | return
157 |
158 | #
159 | # Layer für die Legende ausfindig machen
160 | #
161 |
162 | # You would just need to make sure your layer has a name you can distinguish from others. Instead of:
163 | # Vorherige Version:
164 | #active_raster_layer = self.iface.activeLayer()
165 |
166 | # do:
167 | l_layer = project.mapLayersByName(layer_name)
168 |
169 | if not l_layer:
170 | self.out(f"legend: no layer found with name '{layer_name}'!", False)
171 | return
172 |
173 | active_raster_layer = l_layer[0]
174 |
175 | #print("Legend active_raster_layer id:", active_raster_layer.id()) # ok
176 | #print("Legend active_raster_layer name:", active_raster_layer.name()) # ok
177 | #legend.model().setLayerSet([layer.id() for layer in layers])
178 | #legend.model().setLayerSet([active_raster_layer.id()]) # bringt nichts
179 | # DAS ist es! Dies fügt zumindest erstmal das interessierende Rasterlayer hinzu:
180 | #legend.modelV2().rootGroup().addLayer(active_raster_layer)
181 | #legend.updateLegend()
182 |
183 | #for layout in layout_manager.printLayouts(): # iterate layouts
184 |
185 | ''' would be ok, if we want to create a new legend -> then legend appears at the upper left corner
186 | legend = QgsLayoutItemLegend(layout)
187 | #legend.setTitle('Legend')
188 | legend.setAutoUpdateModel(False)
189 | group = legend.model().rootGroup()
190 | group.clear()
191 | group.addLayer(active_raster_layer)
192 | layout.addItem(legend)
193 | legend.adjustBoxSize()
194 | #legend.refresh() # avoids adding all other layers
195 | '''
196 |
197 | # uses existing legend object (see above), so we preserve it's layout position:
198 | legend.setAutoUpdateModel(False)
199 | group = legend.model().rootGroup()
200 | group.clear()
201 | group.addLayer(active_raster_layer)
202 | legend.adjustBoxSize()
203 |
204 | """ By default the newly created composer items have zero position (top left corner of the page) and zero size.
205 | The position and size are always measured in millimeters.
206 | # set label 1cm from the top and 2cm from the left of the page
207 | composerLabel.setItemPosition(20, 10)
208 | # set both label’s position and size (width 10cm, height 3cm)
209 | composerLabel.setItemPosition(20, 10, 100, 30)
210 | A frame is drawn around each item by default. How to remove the frame:
211 | composerLabel.setFrame(False)
212 | """
213 | #print(active_composer.rect().width(), active_composer.rect().height()) # 1054 911
214 | #print(self.iface.mapCanvas().size().width(), self.iface.mapCanvas().size().height()) # 1517 535
215 | # "Leinwandgröße": habe keine vernünftigen Werte oben ermittelt (vielleicht Pixel; ich brauche mm).
216 | # selbst, mittels Mauszeiger ermittelt:
217 | width = 210
218 | height = 297
219 | # Rand neben der Legende (mm):
220 | dw = 10
221 | dh = 14
222 |
223 | """
224 | Doesn't work since QGIS 3:
225 | """
226 | #self.out("In QGIS 3 the print layout doesn't work anymore. If you can do it ...", False)
227 |
228 | # nothing works
229 | #legendSize = legend.paintAndDetermineSize(None)
230 | #legend.setItemPosition(width - legendSize.width() - dw, height - legendSize.height() - dh)
231 | #legend.setItemPosition(width - legend.width() - dw, height - legend.height() - dh)
232 |
233 | # Also note that active_composer.composerWindow() has a hide() and show()
234 | #active_composer.composerWindow().hide() # works
235 |
236 | def _create_qdocument_from_print_template_content(self):
237 | print_template = self._model.default_print_template
238 |
239 | self.out(f"_create_qdocument_from_print_template_content(): {print_template}")
240 |
241 | if not print_template:
242 | raise FileNotFoundError(f"{print_template}")
243 |
244 | # Load template
245 | with print_template.open() as templateFile:
246 | print_template_content = templateFile.read()
247 |
248 | q_xmldoc = QDomDocument()
249 | # If namespaceProcessing is true, the parser recognizes namespaces in the XML file and sets
250 | # the prefix name, local name and namespace URI to appropriate values. If namespaceProcessing
251 | # is false, the parser does no namespace processing when it reads the XML file.
252 | q_xmldoc.setContent(print_template_content, False) # , bool namespaceProcessing
253 |
254 | return q_xmldoc
255 |
--------------------------------------------------------------------------------
/classes/gui.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import sys
3 | from datetime import datetime
4 |
5 | from qgis.PyQt import QtGui, QtWidgets, uic
6 | from qgis.PyQt.QtCore import pyqtSignal
7 | #from qgis.PyQt.QtWidgets import QFileDialog
8 | # QWidget, QListWidget, QGridLayout, QPushButton
9 |
10 | # not in 'classes', but directly in plugin folder:
11 | plugin_dir = Path(__file__).resolve().parent.parent
12 |
13 | image_dir = plugin_dir / 'img'
14 |
15 | """
16 | Widget with the plugin functionality (DockWidget):
17 | """
18 | WIDGET_FORM_CLASS, _ = uic.loadUiType(plugin_dir / 'dock_widget.ui')
19 |
20 |
21 | def get_icon(img_basename):
22 | """simplifies creating a QIcon"""
23 | img_full_path = image_dir / img_basename
24 | return QtGui.QIcon(str(img_full_path))
25 |
26 | def get_image(img_basename):
27 | """simplifies creating a Image"""
28 | img_full_path = image_dir / img_basename
29 | return QtGui.QPixmap(str(img_full_path))
30 |
31 |
32 | class DockWidget(QtWidgets.QDockWidget, WIDGET_FORM_CLASS):
33 | """ DockWidget
34 | -----------------
35 | begin: 2016-08-26
36 | last: 2019-11
37 | """
38 |
39 | # Indices of tabs: so that someone can easily change the order of the tabs
40 | TAB_RADOLAN_LOADER = 0
41 | TAB_RADOLAN_ADDER = 1
42 | TAB_REGNIE = 2
43 | TAB_STATISTICS = 3
44 | TAB_SETTINGS = 4
45 | TAB_ABOUT = 5
46 |
47 | closingPlugin = pyqtSignal()
48 |
49 |
50 | def __init__(self, parent=None):
51 | """ Constructor. """
52 | super(DockWidget, self).__init__(parent)
53 | # Set up the user interface from Designer.
54 | # After setupUI you can access any designer object by doing
55 | # self., and you can use autoconnect slots - see
56 | # http://doc.qt.io/qt-5/designer-using-a-ui-file.html
57 | # #widgets-and-dialogs-with-auto-connect
58 | self.setupUi(self)
59 |
60 | self.setFloating(False) # prevent losing the widget
61 | # -> doesn't seem to have an effect -> deselected this property in the .ui-file
62 | # seems that is impossible to set this as initial property in QT Creator.
63 | """ If you want to prevent the user from moving it to a floating window
64 | you need to set the "features" of the widget. In the example below,
65 | the widget is movable and closable, but not floatable: """
66 | #self.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable | QtWidgets.QDockWidget.DockWidgetMovable)
67 |
68 | self.btn_close.clicked.connect(self.close) # global close button
69 | self.btn_close.setIcon(get_icon('close.png'))
70 |
71 | self._tab_index = 0
72 | self.tabWidget.currentChanged.connect(self._tab_changed)
73 |
74 | ############################
75 | # Tab "RADOLAN single mode"
76 | ############################
77 |
78 | folder_icon = get_icon('folder.png')
79 |
80 | self.tabWidget.setTabIcon(DockWidget.TAB_RADOLAN_LOADER, get_icon('execute.png'))
81 | # set toolbar button icons:
82 | self.btn_load_project.setIcon(get_icon('new.png'))
83 | self.btn_load_radars.setIcon(get_icon('radar.png'))
84 |
85 | self.filedialog_input.setIcon(folder_icon)
86 | self.filedialog_mask.setIcon(folder_icon)
87 | self.filedialog_qml.setIcon(folder_icon)
88 |
89 | self.widget_symb.setVisible(False)
90 | # Only enabled for RX products (check at every load):
91 | self.check_rvp6tomm.setVisible(False)
92 |
93 | # connect functions:
94 | #self.btn_info.clicked.connect(self.open_about_dialog)
95 | # passing parameters to connected method only possible with keyword 'lambda':
96 | self.check_cut.stateChanged.connect(lambda: self._checkbox_state_changed(self.check_cut))
97 | self.check_symb.stateChanged.connect(lambda: self._checkbox_state_changed(self.check_symb))
98 | self.check_rvp6tomm.stateChanged.connect(lambda: self._checkbox_state_changed(self.check_rvp6tomm))
99 |
100 | #if not self.dock.inputpath.text():
101 | # #self.dock.button_box.button(QDialogButtonBox.Cancel).setEnabled(True)
102 | # self.dock.button_box.button(QDialogButtonBox.Apply).setEnabled(False)
103 | # #self.out("OK button disabled -> please load a RADOLAN binary file first!")
104 | self.btn_action.setIcon(get_icon('execute.png'))
105 | self.btn_action.setEnabled(False) # initially disabled, need to load RADOLAN file
106 |
107 | # trigger deactivating clipping:
108 | self.check_cut.setChecked(False)
109 | self._checkbox_state_changed(self.check_cut)
110 |
111 | ############################
112 | # Tab Statistics
113 | ############################
114 | tab_no = DockWidget.TAB_STATISTICS
115 | #self.tabWidget.setTabEnabled(tab_no, False) # second tab "statistics"
116 | # -> only disabled, still visible, grayed
117 | #self.tabWidget.setTabVisible(tab_no, False) # direct, but see comment in method
118 | self.set_statistics_tab_visible(False) # this makes it invisible first
119 | self.tabWidget.setTabIcon(tab_no, get_icon('stats.png'))
120 |
121 | ############################
122 | # Tab TIF storage
123 | ############################
124 |
125 | self.tabWidget.setTabIcon(DockWidget.TAB_SETTINGS, get_icon('execute.png'))
126 | self.btn_select_storage_dir.setIcon(folder_icon)
127 | self.btn_save.setIcon(get_icon('save.png'))
128 | # save button is disabled by default
129 | self.btn_save.setEnabled(False)
130 |
131 | #self.setWindowIcon(get_icon('folder.png'))
132 |
133 | ############################
134 | # Tab REGNIE
135 | ############################
136 |
137 | self.tabWidget.setTabIcon(DockWidget.TAB_REGNIE, get_icon('regnie.png'))
138 | self.btn_select_regnie.setIcon(folder_icon)
139 | self.btn_load_regnie.setIcon(get_icon('regnie.png'))
140 |
141 | ############################
142 | # Tab "RADOLANAdder"
143 | ############################
144 |
145 | self.tabWidget.setTabIcon(DockWidget.TAB_RADOLAN_ADDER, get_icon('stack.png'))
146 | self.btn_select_dir_adder.setIcon(folder_icon)
147 | self.btn_scan.setIcon(get_icon('search.png'))
148 | self.btn_run_adder.setIcon(get_icon('execute.png'))
149 |
150 | ############################
151 | # Tab "about"
152 | ############################
153 |
154 | self.tabWidget.setTabIcon(DockWidget.TAB_ABOUT, get_icon('info.png'))
155 |
156 | # insert images:
157 | dt_today = datetime.today()
158 | # Christmas period?
159 | if dt_today.month == 12 and 20 <= dt_today.day <= 31:
160 | # set QMovie as label:
161 | movie = QtGui.QMovie(str(image_dir / 'weihnachten.gif'))
162 | # set 'ScaledContents' in QtDesigner to False or self.label_logo.setScaledContents(False)
163 | self.label_logo.setMovie(movie)
164 | movie.start()
165 | else:
166 | self.label_logo.setPixmap(get_image('plugin_logo.png'))
167 |
168 | self.label_img_info.setPixmap(get_image('sw_info.png'))
169 | self.label_img_download.setPixmap(get_image('sw_download.png'))
170 |
171 | # fill text fields with metadata:
172 | self._fill_fields()
173 | ############################
174 |
175 | def __str__(self):
176 | return self.__class__.__name__
177 |
178 | def out(self, s, ok=True):
179 | if ok:
180 | print(f"{self}: {s}")
181 | else:
182 | print(f"{self}: {s}", file=sys.stderr)
183 |
184 | def _checkbox_state_changed(self, checkbox):
185 | name = checkbox.objectName()
186 | b = checkbox.isChecked()
187 |
188 | # Diag:
189 | #self.out("_checkbox_state_changed() from '{}': {}".format(name, b))
190 |
191 | if name == 'check_cut':
192 | self.inputmask.setEnabled(b)
193 | self.filedialog_mask.setEnabled(b)
194 | elif name == 'check_symb':
195 | self.widget_symb.setVisible(b)
196 |
197 | def _tab_changed(self):
198 | index = self.tabWidget.currentIndex()
199 |
200 | # save index only, if it is a relevant function tab:
201 | if index != DockWidget.TAB_STATISTICS and index != DockWidget.TAB_ABOUT:
202 | self._tab_index = index
203 | #msg = f"Tab index changed! Save current tab index: {index}"
204 | #self.out(msg)
205 |
206 | def closeEvent(self, event):
207 | self.closingPlugin.emit()
208 | event.accept()
209 |
210 | def _fill_fields(self):
211 | metadata_file = plugin_dir / 'metadata.txt'
212 |
213 | version = "?"
214 | issue_tracker = "?"
215 | mail_link = "?"
216 |
217 | self.out("reading '{}'".format(metadata_file))
218 |
219 | with metadata_file.open() as f:
220 | for line in f:
221 | """ filter lines from metadata file:
222 | version=0.6
223 | email=radolan2map@e.mail.de
224 | tracker=https://gitlab.com/Weatherman_/radolan2map/issues
225 | """
226 |
227 | if line.startswith('version'):
228 | version = self.__get_value(line)
229 | elif line.startswith('email'):
230 | mailadress = self.__get_value(line)
231 | mail_link = f'{mailadress}'
232 | elif line.startswith('tracker'):
233 | issue_link = self.__get_value(line)
234 | issue_tracker = f'{issue_link}'
235 | # for
236 | # with
237 |
238 | self.text_version.setText(version)
239 | self.text_issue.setText(issue_tracker)
240 | self.text_mailaddress.setText(mail_link)
241 |
242 | def __get_value(self, line):
243 | return line.strip().split('=')[1] # version=0.6
244 |
245 | def set_statistics_tab_visible(self, b_visible):
246 | self.out(f"set statistics tab visibility = {b_visible}")
247 | no = DockWidget.TAB_STATISTICS
248 | try:
249 | self.tabWidget.setTabVisible(no, b_visible)
250 | # Problem with an older Qt version(?) occured on Windows with QGIS 3.18,
251 | # in 3.24 Prizren it works.
252 | # Tab visibility has been introduced in Qt5.15
253 | # AttributeError: 'QTabWidget' object has no attribute 'setTabVisible'
254 | except AttributeError as e:
255 | self.out(f"Exception catched:\n{e}", False)
256 | # so we must use another method instead, we cannot hide it:
257 | self.tabWidget.setTabEnabled(no, b_visible)
258 |
259 | if b_visible:
260 | self.tabWidget.setCurrentIndex(no)
261 |
262 | @property
263 | def tab_index(self):
264 | return self._tab_index
265 |
266 | @tab_index.setter
267 | def tab_index(self, i):
268 | self.tabWidget.setCurrentIndex(i)
269 |
--------------------------------------------------------------------------------
/classes/NumpyRadolanAdder.py:
--------------------------------------------------------------------------------
1 | '''
2 | Created on 27.11.2020
3 |
4 | @author: Weatherman
5 | '''
6 |
7 | import sys
8 | from pathlib import Path
9 | from glob import glob
10 | from copy import copy
11 | from datetime import datetime, timedelta
12 | import numpy as np
13 | import warnings
14 | import re
15 |
16 | from .NumpyRadolanReader import NumpyRadolanReader # Input
17 | from .ASCIIGridWriter import ASCIIGridWriter # Output
18 |
19 |
20 |
21 | def test_sum2D_with_nan():
22 | print("### test_sum2D_with_nan() ###\n")
23 |
24 | # Creates a 2D array with uninitialized values
25 | a = np.ndarray((2, 4))
26 | # Datentyp ist Default: 'float64'
27 | a.fill(7) # Fill it in with a constant value 7
28 | # auch: B.fill(numpy.nan) # ! nur float-Array
29 | a[1][0] = np.nan
30 | a[1][1] = np.nan
31 | print("a =\n", a)
32 |
33 | b = np.ndarray((2, 4))
34 | b.fill(1.)
35 | b[1][1] = np.nan
36 | print("b =\n", b)
37 |
38 | """
39 | Since the inputs are 2D arrays, you can stack them along the third axis with
40 | np.dstack and then use np.nansum which would ensure NaNs are ignored, unless
41 | there are NaNs in both input arrays, in which case output would also have NaN.
42 | -> The behavior that NaN is returned when both inputs are NaN
43 | changed for NumPy version >1.9.0. In the newer versions, zero is returned!
44 | """
45 | c = np.nansum(np.dstack((a, b)), 2)
46 |
47 | print("c =\n", c)
48 |
49 |
50 | def test_sum_same_radolan_file():
51 | print("### test_sum_same_radolan_file() ###\n")
52 |
53 | test_data_dir = "/run/media/loki/ungesichert/Testdaten/RADOLAN_bin"
54 |
55 | rw = Path(test_data_dir) / "bin/raa01-rw_10000-1109041250-dwd---bin.gz"
56 |
57 | radolan_file = rw
58 |
59 | row = 450
60 |
61 | nrr = NumpyRadolanReader(radolan_file) # FileNotFoundError
62 | nrr.read()
63 | #nrr.print_data()
64 | f1 = nrr.data
65 | f2 = copy(f1)
66 |
67 | print(f1[row])
68 | print()
69 |
70 | """
71 | Since the inputs are 2D arrays, you can stack them along the third axis with
72 | np.dstack and then use np.nansum which would ensure NaNs are ignored, unless
73 | there are NaNs in both input arrays, in which case output would also have NaN.
74 | -> The behavior that NaN is returned when both inputs are NaN
75 | changed for NumPy version >1.9.0. In the newer versions, zero is returned!
76 | """
77 | fsum = np.nansum(np.dstack((f1, f2)), 2)
78 |
79 | print(fsum[row])
80 |
81 |
82 |
83 |
84 | class NumpyRadolanAdder:
85 | '''
86 | classdocs
87 | '''
88 |
89 | def __init__(self, dt_beg, dt_end, data_path, prod_id, asc_filename_path):
90 | '''
91 | Constructor
92 | '''
93 |
94 | self.out("dt_beg={}, dt_end={}, prod_id='{}'".format(dt_beg, dt_end, prod_id))
95 |
96 | # In:
97 | self._dt_beg = dt_beg
98 | self._dt_end = dt_end
99 | self._data_path = data_path
100 | self._prod_id = prod_id.lower() # 'SF' -> 'sf'
101 | self._asc_filename_path = asc_filename_path
102 |
103 | self._first_run = True
104 |
105 | # determined:
106 | self._interval_minutes = 0 # of sum
107 |
108 | # Out:
109 | self._sum_field = None
110 | self._nodata_field = None
111 |
112 |
113 | def __str__(self):
114 | return self.__class__.__name__
115 |
116 | def out(self, s, ok=True):
117 | ''' Ausgabemethode '''
118 |
119 | if ok:
120 | print("{}: {}".format(self, s))
121 | else:
122 | print("{}: {}".format(self, s), file=sys.stderr)
123 |
124 |
125 |
126 | def run(self):
127 | self.out("run()")
128 |
129 | # RADOLAN: raa01-rw_10000-1708020250-dwd---bin
130 | # RADKLIM: raa01-yw2017.002_10000-1006010650-dwd---bin
131 | # ...bin, ...bin.gz ?
132 | general_radolan_file_pattern = "raa01-{}*_10000-{{}}-dwd---bin*".format(self._prod_id)
133 |
134 | pattern_all_files_same_type = general_radolan_file_pattern.format('*')
135 | concrete_radolan_file_pattern = str( Path(self._data_path) / pattern_all_files_same_type )
136 | print(" determine files...", end=" ")
137 | l_files_all_same_type = glob(concrete_radolan_file_pattern)
138 | print("{} files of '{}' type found".format(len(l_files_all_same_type), self._prod_id.upper()))
139 |
140 | if not l_files_all_same_type:
141 | raise FileNotFoundError("no RADOLAN files for adding found!")
142 |
143 | # simply take first file to determine properties one time:
144 | radolan_file = l_files_all_same_type[0]
145 | time_res_min, prec = self._read_first_file_init(radolan_file)
146 |
147 |
148 | """
149 | Following complicated way because we don't know, if user adds standard
150 | RADOLAN data or RADKLIM data or wether the files are gzip compressed.
151 | The file patterns are slightly different.
152 | So we create a concrete and surely existing file pattern here.
153 | """
154 | # raa01-xx_10000-{}-dwd---bin[.gz] # insert '{}' for datetime
155 | general_radolan_file_pattern = re.sub(r'(\d){10}', '{}', radolan_file)
156 | general_radolan_file_pattern_with_path = str( Path(self._data_path) / general_radolan_file_pattern )
157 | # -> so now, we need only to substitute the placeholder {} for datetime.
158 |
159 | # datetime.timedelta([days[, seconds[, microseconds[, milliseconds[, minutes[, hours[, weeks]]]]]]])
160 | td_min = timedelta(minutes=time_res_min)
161 |
162 | dt = self._dt_beg
163 | number_of_added_files = 0
164 |
165 | while dt <= self._dt_end:
166 | fn_path = general_radolan_file_pattern_with_path.format( dt.strftime("%y%m%d%H%M") )
167 |
168 | if fn_path in l_files_all_same_type:
169 | self._add(fn_path)
170 | number_of_added_files += 1
171 | else:
172 | self.out("expected file '{}' doesn't exist".format(concrete_radolan_file_pattern), False)
173 |
174 | dt += td_min
175 | # while
176 |
177 | #print(self._sum_field[0])
178 | #print(self._nodata_field[0])
179 |
180 | self.out("{} files added".format(number_of_added_files))
181 |
182 | # Imprint NaN values that were NaN during the entire run:
183 | nan_positions = np.where(np.isnan(self._nodata_field)) # find the remaining NaNs
184 | self._sum_field[nan_positions] = np.nan
185 |
186 | ascii_writer = ASCIIGridWriter(self._sum_field, prec, self._asc_filename_path)
187 | ascii_writer.write()
188 |
189 |
190 | def _read_first_file_init(self, radolan_file):
191 | self.out("init with first file")
192 |
193 | nrr = NumpyRadolanReader(radolan_file) # FileNotFoundError
194 | nrr._read_radolan_composite(loaddata=False) # optimize, no reading of data part neccessary
195 |
196 | # Return a new array of given shape and type, filled with fill_value.
197 | self._nodata_field = np.full(nrr.shape, np.nan) # all with NaN
198 |
199 | time_res_min = nrr.interval
200 | print(" 'time_res' determined: {} minutes".format(time_res_min))
201 |
202 | return time_res_min, nrr.precision
203 |
204 |
205 |
206 | def _add(self, radolan_file):
207 | #self.out("_add('{}')".format(radolan_file))
208 |
209 | nrr = NumpyRadolanReader(radolan_file) # FileNotFoundError
210 | nrr.read()
211 | cur_data = nrr.data
212 |
213 | self._interval_minutes += nrr.interval
214 |
215 | # every occurence of a pixel (for every looped file) switches a 'nan' to a 'True':
216 | # np.where return indices where the conditions are true
217 | indices_non_nan = np.where(~np.isnan(cur_data))
218 | # switch non nan positions to True. For every loop.
219 | self._nodata_field[indices_non_nan] = True
220 |
221 | if self._first_run:
222 | self._sum_field = cur_data # Init
223 | self._first_run = False
224 | return
225 |
226 | """
227 | Since the inputs are 2D arrays, you can stack them along the third axis with
228 | np.dstack and then use np.nansum which would ensure NaNs are ignored, unless
229 | there are NaNs in both input arrays, in which case output would also have NaN.
230 | -> The behavior that NaN is returned when both inputs are NaN
231 | changed for NumPy version >1.9.0. In the newer versions, zero is returned!
232 | """
233 | self._sum_field = np.nansum(np.dstack((self._sum_field, cur_data)), 2)
234 |
235 |
236 |
237 | def get_statistics(self):
238 | dim = "rows={}, cols={}".format(*self._sum_field.shape)
239 | _max = np.nanmax(self._sum_field) # Maximum of the flattened array, ignoring Nan
240 | _min = np.nanmin(self._sum_field) # Minimum of the flattened array, ignoring Nan
241 | total = self._sum_field.size
242 | valid = np.count_nonzero(~np.isnan(self._sum_field)) # ~ inverts the boolean matrix returned from np.isnan
243 | nonvalid = np.count_nonzero(np.isnan(self._sum_field))
244 |
245 | return dim, _max, _min, self.mean, total, valid, nonvalid
246 |
247 | @property
248 | def mean(self):
249 | """ nanmean: The arithmetic mean is the sum of the non-NaN elements along the axis
250 | divided by the number of non-NaN elements.
251 | When the array has nothing but nan values, it raises a warning:
252 | /usr/lib64/python3.4/site-packages/numpy/lib/nanfunctions.py:675: RuntimeWarning: Mean of empty slice
253 | warnings.warn("Mean of empty slice", RuntimeWarning)
254 | """
255 |
256 | # necessary to catch the warning like an exception:
257 | # To handle warnings as errors simply use this:
258 | warnings.filterwarnings('error')
259 |
260 | # In order to be able to react to this state, the warning is intercepted:
261 | try:
262 | return np.nanmean(self._sum_field)
263 |
264 | except Warning:
265 | self.out("Warning catched: return np.nan", False)
266 | return np.nan
267 |
268 | @property
269 | def interval_minutes(self):
270 | return self._interval_minutes
271 |
272 |
273 | #################################################################
274 |
275 |
276 | def test_nan():
277 | '''
278 | Tests, if NaN's of last added file don't overwrite valid values of the preceding file
279 | Does result preserve the values of 'f1'?
280 | '''
281 |
282 | test_dir = "/run/media/loki/ungesichert/Testdaten/QGIS/radolan2map/nan-test"
283 | asc_filename_path = Path(test_dir) / "nan_test.asc"
284 |
285 | f1 = Path(test_dir) / "raa01-sf_10000-1807060550-dwd---bin" # more data in the north
286 | f2 = Path(test_dir) / "raa01-sf_10000-1501020550-dwd---bin" # lesser data in the north
287 |
288 |
289 | adder = NumpyRadolanAdder(None, None, test_dir, 'SF', asc_filename_path)
290 | # call internal methods directly:
291 | adder._read_first_file_init(f1) # init, no adding
292 |
293 | adder._add(f1)
294 | adder._add(f2)
295 |
296 | sum_field = adder._sum_field # fetch reference
297 |
298 | # Imprint NaN values that were NaN during the entire run:
299 | nan_positions = np.where(np.isnan(adder._nodata_field)) # find the remaining NaNs
300 | sum_field[nan_positions] = np.nan
301 |
302 | ascii_writer = ASCIIGridWriter(sum_field, 0.1, asc_filename_path)
303 | ascii_writer.write()
304 |
305 |
306 |
307 | if __name__ == '__main__':
308 |
309 | #test_sum2D_with_nan()
310 | #test_sum_same_radolan_file()
311 | #test_nan(); sys.exit()
312 |
313 |
314 | data_path = "/run/media/loki/ungesichert/Testdaten/RADOLAN_bin/viele/sf"
315 | prod_id = 'SF'
316 | asc_filename_path = "/home/loki/radolan2map/tmp/SF_201501010550-201501070550.asc"
317 |
318 | dt_beg = datetime(2015, 1, 1, 5, 50)
319 | dt_end = datetime(2015, 1, 7, 5, 50)
320 |
321 | adder = NumpyRadolanAdder(dt_beg, dt_end, data_path, prod_id, asc_filename_path)
322 | adder.run()
323 |
324 |
--------------------------------------------------------------------------------
/classes/ActionTabRADOLANAdder.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from pathlib import Path
4 | from copy import copy
5 | from datetime import datetime
6 | import re
7 |
8 | from qgis.PyQt.QtWidgets import QFileDialog
9 | from qgis.PyQt.QtCore import QDateTime
10 |
11 | # own classes:
12 | from .ActionTabBase import ActionTabBase # base class
13 | from .NumpyRadolanAdder import NumpyRadolanAdder
14 | from .GDALProcessing import GDALProcessing
15 | from .LayerLoader import LayerLoader
16 |
17 | """
18 | The format of date and datetime is different from the format of QDate and QDateTime,
19 | you should not use % in the Qt format!
20 | """
21 | df_dt = '%Y-%m-%d %H:%M'
22 | df_qt = 'yyyy-MM-dd hh:mm'
23 |
24 |
25 | class ActionTabRADOLANAdder(ActionTabBase):
26 | """ActionTabRADOLANAdder
27 | Separate __actions__ of RADOLAN summation operations from the other components
28 |
29 | Created on 02.12.2020
30 | @author: Weatherman """
31 |
32 | def __init__(self, iface, model, dock):
33 | """
34 | Constructor
35 | """
36 |
37 | super().__init__(iface, model, dock)
38 |
39 | # Listener
40 | dock.btn_select_dir_adder.clicked.connect(self._select_input_dir)
41 | dock.btn_scan.clicked.connect(self._scan_for_products)
42 | dock.btn_run_adder.clicked.connect(self._run)
43 | dock.btn_set_datetime.clicked.connect(self._set_begin_end_automatically)
44 | dock.listWidget.itemSelectionChanged.connect(self._listwidget_selection_changed) # itemClicked.connect
45 |
46 | # setting date time
47 | dt = QDateTime.currentDateTime()
48 | self.dock.dateTimeEdit_beg.setDateTime(dt)
49 | self.dock.dateTimeEdit_end.setDateTime(copy(dt))
50 |
51 | self._prod_id = None
52 | self._files = None # list of scanned RADOLAN files
53 |
54 | # for saving:
55 | self._begin = None
56 | self._end = None
57 | self._last_dir = None
58 |
59 | """
60 | Actions
61 | """
62 |
63 | def _select_input_dir(self):
64 | text = self.tf_path
65 | self._last_dir = text if text else str(Path.home())
66 |
67 | # parent, caption, directory, file_filter
68 | selected_dir = QFileDialog.getExistingDirectory(self.dock, 'Select RADOLAN directory', self._last_dir,
69 | # these additional parameters are used, because QFileDialog otherwise doesn't start with the given path:
70 | QFileDialog.DontUseNativeDialog)
71 |
72 | if not selected_dir:
73 | return # preserve possibly filled line
74 |
75 | self.tf_path = selected_dir
76 |
77 | # automatically scan for products
78 | self._scan_for_products()
79 | # but therefore disable the manual scan button:
80 | self.dock.btn_scan.setEnabled(False)
81 |
82 | def _scan_for_products(self):
83 | self.out("_scan_for_products()")
84 |
85 | # an error could occur or user needs to select an item (found product) first
86 | self.dock.btn_run_adder.setEnabled(False)
87 |
88 | lwidget = self.dock.listWidget # shorten
89 | lwidget.clear()
90 |
91 | scan_path = Path(self.tf_path)
92 | print(f" scan path: '{scan_path}'")
93 |
94 | try:
95 | files = scan_path.glob('raa01-*---bin*') # possibly '.gz'
96 | if not files:
97 | self.dock.btn_set_datetime.setEnabled(False)
98 | raise FileNotFoundError("No RADOLAN products found!")
99 | except Exception as e:
100 | super()._show_critical_message_box(str(e))
101 | return
102 |
103 | # set:
104 | self._files = list(files) # save list of Posixpath because of generator running o.o.i.
105 | # sort because order of RADOLAN first and last file for setting datetime:
106 | self._files.sort()
107 |
108 | self.dock.btn_set_datetime.setEnabled(True)
109 |
110 | # Detect product IDs and count:
111 |
112 | d_id = {} # dict of product ids
113 | # raa01-sf_10000-1501070550-dwd---bin -> 'SF'
114 | for f in self._files: # of Posixpath
115 | _id = f.name[6:8].upper()
116 |
117 | if _id[1] == 'X': # exclude RVP-products (1 Byte) like 'RX', 'WX', 'EX', ...
118 | print(f"- exclude '{_id}'")
119 | continue
120 |
121 | try: # if key exist, increment
122 | d_id[_id] += 1
123 | except KeyError:
124 | d_id[_id] = 1
125 | # for files
126 |
127 | l_id = [] # list of product ids
128 | for k, v in d_id.items():
129 | print(f"{k}: {v}")
130 | if v > 1:
131 | l_id.append(k)
132 | # for
133 |
134 | self.out(f"IDs: {l_id}")
135 |
136 | for i in l_id:
137 | lwidget.addItem(i)
138 |
139 | # again, because '_listwidget_selection_changed()' enables 'btn_run_adder':
140 | self.dock.btn_run_adder.setEnabled(False)
141 |
142 | def _set_begin_end_automatically(self):
143 | if not self._files:
144 | raise FileNotFoundError("No RADOLAN products in list!")
145 |
146 | first_file = self._files[0].name
147 | last_file = self._files[-1].name
148 |
149 | # RADOLAN: ['01', '10000', '1605290050']
150 | # RADKLIM: ['01', '2017', '002', '10000', '1806010050']
151 | digits_begin = re.findall(r'\d+', first_file)[-1]
152 | digits_end = re.findall(r'\d+', last_file)[-1]
153 |
154 | # save this. These are the settings with which the user executed:
155 | self._begin = "20" + digits_begin
156 | self._end = "20" + digits_end
157 |
158 | df = '%Y%m%d%H%M'
159 | dt_beg = datetime.strptime(self._begin, df)
160 | dt_end = datetime.strptime(self._end, df)
161 | print("Begin:", dt_beg)
162 | print("End: ", dt_end)
163 |
164 | self.dock.dateTimeEdit_beg.setDateTime(dt_beg)
165 | self.dock.dateTimeEdit_end.setDateTime(dt_end)
166 |
167 | self.dock.btn_set_datetime.setEnabled(False)
168 |
169 | def _listwidget_selection_changed(self):
170 | self.dock.btn_run_adder.setEnabled(True)
171 | item = self.dock.listWidget.currentItem().text()
172 | #print(item)
173 | self._prod_id = item
174 | self.__setup_suitable_datetime_for_selected_product(item)
175 |
176 | def __setup_suitable_datetime_for_selected_product(self, prod_id):
177 | # self.dock.dateTimeEdit_beg.dateTime()): PyQt5.QtCore.QDateTime(2020, 12, 3, 20, 40, 56, 553)
178 | #df = "dd.MM.yyyy HH:mm"
179 | #print("Beginn:", self.dock.dateTimeEdit_beg.dateTime().toString(df))
180 | #print("Ende: ", self.dock.dateTimeEdit_end.dateTime().toString(df))
181 | #print(prod_id)
182 |
183 | # QDateTime -> datetime and smooth date:
184 | dt_beg = self.dock.dateTimeEdit_beg.dateTime().toPyDateTime()
185 |
186 | # assume hourly product:
187 | set_min = 50
188 |
189 | # 5 minute product: 'RZ', 'RY', 'YW', 'EZ', 'EY'
190 | if prod_id[1] == 'Y' or prod_id[1] == 'Z' or prod_id == 'YW':
191 | # simple defaults:
192 | if dt_beg.minute > 31:
193 | set_min = 45
194 | elif dt_beg.minute < 30:
195 | set_min = 0
196 | else: # 30
197 | set_min = dt_beg.minute
198 | # if
199 |
200 | dt_new = dt_beg.replace(minute=set_min, second=0, microsecond=0)
201 |
202 | self.begin = dt_new.strftime(df_dt)
203 |
204 | def _run(self):
205 | self.out("_run()")
206 |
207 | dock = self.dock # shorten
208 |
209 | #
210 | # Checks
211 | #
212 |
213 | # Checkbox enabled AND mask shape specified?
214 | mask_file = dock.inputmask.text()
215 | if dock.check_cut.isChecked():
216 | # if the use of mask file will be relevant, check it
217 | if not Path(mask_file).exists():
218 | super()._show_critical_message_box("The specified mask file doesn't exist!", 'File error')
219 | return
220 | else:
221 | mask_file = None
222 |
223 | dock.btn_run_adder.setEnabled(False) # disable run button during operation
224 |
225 | # QDateTime -> datetime and smooth date:
226 | dt_beg = dock.dateTimeEdit_beg.dateTime().toPyDateTime()
227 | dt_end = dock.dateTimeEdit_end.dateTime().toPyDateTime()
228 |
229 | # save this. These are the settings with which the user executed:
230 | self._begin = dt_beg.strftime(df_dt)
231 | self._end = dt_end.strftime(df_dt)
232 | #print("Begin:", dt_beg)
233 | #print("End: ", dt_end)
234 |
235 | # Try to catch every Exception and show it in a graphical window.
236 |
237 | df = '%Y%m%d%H%M'
238 | fn = f"{self._prod_id}_{dt_beg.strftime(df)}-{dt_end.strftime(df)}.asc"
239 | asc_filename_path = self._model.temp_dir / fn
240 |
241 | # for performance reason disable output for many files:
242 | saved_stdout = sys.stdout
243 | saved_stderr = sys.stderr
244 | self.out("disable output for performance reasons, add files...")
245 | f_devnull = open(os.devnull, 'w')
246 | sys.stdout = f_devnull
247 | sys.stderr = sys.stdout # redirect both
248 |
249 | try:
250 | # no cleaning temp, so we can check the temp result after running:
251 | self._model.create_storage_folder_structure(use_temp_dir=True)
252 | adder = NumpyRadolanAdder(dt_beg, dt_end, self.tf_path, self._prod_id, asc_filename_path)
253 | adder.run()
254 | except Exception as e:
255 | super()._show_critical_message_box(str(e))
256 | return
257 | finally:
258 | f_devnull.close()
259 | sys.stderr = saved_stderr
260 | sys.stdout = saved_stdout
261 | self.out("output channels restored")
262 |
263 | """
264 | From here is related to GIS layer loading
265 | """
266 |
267 | # at GDAL processing a lot of strange errors are possible - with projection parameters and GDAL versions...
268 | try:
269 | tif_file = self.__convert_asc_tif(asc_filename_path, mask_file)
270 | except Exception as e:
271 | super()._show_critical_message_box(str(e), 'GDAL processing error')
272 | return
273 |
274 | """
275 | If no project file not loaded when running plugin
276 | """
277 |
278 | super()._check_create_project()
279 |
280 | # Set symbology from QML file:
281 | # 1) as given parameter by user or
282 | # 2) automatically by program
283 | qml_file = None
284 | # self defined symbology:
285 | if dock.check_symb.isChecked():
286 | qml_file = dock.inputqml.text()
287 | # if the use of a user defined qml file will be relevant, check it
288 | if not Path(qml_file).exists():
289 | super()._show_critical_message_box("The specified QML file doesn't exist!", 'File error')
290 | return
291 | # determine QML file automatically:
292 | else:
293 | interval_minutes = adder.interval_minutes # 'interval_minutes' for assigning a color map
294 | # Determine prepared QML-File delivered with the plugin:
295 | qml_file = self._model.qml_file(interval_minutes) # .qml or 'None'
296 |
297 | ll = LayerLoader(self._iface) # 'iface' is from 'radolan2map'
298 |
299 | if dock.check_excl_zeroes.isChecked():
300 | ll.no_zeros = True # Set 0 values to NODATA (= transparent)
301 |
302 | ll.load_raster(tif_file, qml_file, temporal=False)
303 |
304 | dock.btn_run_adder.setEnabled(True) # re-activate
305 |
306 | dim, _max, _min, mean, total, valid, nonvalid = adder.get_statistics()
307 |
308 | s_max = str(_max)
309 | # if part after point is too long:
310 | l_max = s_max.split('.')
311 | if len(l_max[1]) > 2:
312 | s_max = f"{_max:.1f}"
313 |
314 | dock.text_filename.setText(asc_filename_path.stem)
315 | dock.text_shape.setText(dim)
316 | dock.text_max.setText(s_max)
317 | dock.text_min.setText(str(_min))
318 | dock.text_mean.setText(f"{mean:.1f}")
319 | dock.text_total_pixels.setText(str(total))
320 | dock.text_valid_pixels.setText(str(valid))
321 | dock.text_nonvalid_pixels.setText(str(nonvalid))
322 |
323 | super()._enable_and_show_statistics_tab()
324 | super()._finish()
325 |
326 | super()._load_print_layout(ll.layer_name, prod_id='Sum') # dt=None
327 |
328 | def __convert_asc_tif(self, asc_filename_path, mask_file=None):
329 | """raise Exception
330 | At GDAL processing a lot of strange errors are possible - with projection parameters and GDAL versions...
331 | """
332 | model = self._model # shorten
333 |
334 | model.set_data_dir("sum")
335 | model.create_storage_folder_structure()
336 |
337 | tif_bn = asc_filename_path.name.replace('.asc', '.tif') # tif basename
338 | tif_filename_path = model.data_dir / tif_bn
339 |
340 | gdal_processing = GDALProcessing(model, asc_filename_path, tif_filename_path)
341 | #gdal_processing.produce_warped_tif_using_script()
342 |
343 | #l_elems = self.dock.cbbox_projections.currentText().split() # EPSG:3035 ETRS89 / LAEA Europe
344 | #epsg_code = l_elems[0]
345 | #if epsg_code == '-': # RADOLAN
346 | # epsg_code = model.projection_radolan # complete RADOLAN projection parameters
347 | prj_src = model.projection_radolan
348 | """ Finding the content of current item in combo box:
349 | Laborious over index, because otherwise the value of combo box would
350 | EPSG 3035: ETRS89 / LAEA Europe instead of
351 | EPSG:3035 """
352 | index = self.dock.cbbox_projections.currentIndex()
353 | prj_dest = model.projections[index]
354 | #prj_dest_test = self.dock.cbbox_projections.currentText()
355 | #self.out("projection (currentText): {}".format(prj_dest_test))
356 |
357 | gdal_processing.produce_warped_tif_by_python_gdal(prj_src, prj_dest, shapefile=mask_file) # Exception
358 |
359 | return gdal_processing.tif_file
360 |
361 | # ......................................................................
362 |
363 | @property
364 | def tf_path(self):
365 | return self.dock.tf_path_adder.text()
366 | @tf_path.setter
367 | def tf_path(self, path):
368 | self.dock.tf_path_adder.setText(path)
369 | # after folder selection enable scan button:
370 | self.dock.btn_scan.setEnabled(True)
371 | self._last_dir = path
372 |
373 | @property
374 | def begin(self):
375 | return self._begin
376 | @begin.setter
377 | def begin(self, beg):
378 | self._begin = beg
379 | dt = QDateTime.fromString(beg, df_qt)
380 | self.dock.dateTimeEdit_beg.setDateTime(dt)
381 |
382 | @property
383 | def end(self):
384 | return self._end
385 | @end.setter
386 | def end(self, end):
387 | self._end = end
388 | dt = QDateTime.fromString(end, df_qt)
389 | self.dock.dateTimeEdit_end.setDateTime(dt)
390 |
391 | @property
392 | def last_dir(self):
393 | return self._last_dir
394 |
--------------------------------------------------------------------------------
/classes/LayerLoader.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import sys
4 | from pathlib import Path
5 | from datetime import datetime, timedelta
6 | from PyQt5.QtCore import Qt
7 |
8 | from qgis.core import (
9 | Qgis,
10 | QgsProject,
11 | QgsLayerTreeLayer,
12 | QgsRasterTransparency,
13 | QgsVectorLayer,
14 | QgsRasterLayer,
15 | QgsDateTimeRange,
16 | QgsRasterLayerTemporalProperties,
17 | )
18 | import processing
19 |
20 |
21 | class LayerLoader:
22 | """
23 | LayerLoader
24 |
25 | Loads creates and load a QgsRaster- or QgsVectorLayer
26 |
27 | Created on 07.12.2020
28 | @authors: Weatherman, Tobias Rosskopf
29 | """
30 |
31 | def __init__(self, iface):
32 | print(self)
33 |
34 | self._iface = iface
35 |
36 | # set or determined later:
37 | self._no_zeros = False # set zeros invisible
38 | self._layer_name = None
39 | self._temporal = None
40 |
41 | def __str__(self):
42 | return self.__class__.__name__
43 |
44 | def out(self, s, ok=True):
45 | if ok:
46 | print(f"{self}: {s}")
47 | else:
48 | print(f"{self}: {s}", file=sys.stderr)
49 |
50 | def _show_message(self, qgis_state, layer_name, duration=0):
51 | """ Where the integer 0 indicates a no timeout (i.e. no duration). """
52 |
53 | if qgis_state == Qgis.Success:
54 | title = "Success"
55 | msg = f'Layer "{layer_name}" loaded!'
56 | else:
57 | title = "Error"
58 | msg = f'Layer "{layer_name}" failed to load!'
59 | self.out(msg, False)
60 |
61 | self._iface.messageBar().pushMessage(title, msg, level=qgis_state, duration=duration)
62 |
63 | def create_and_load_layer_group(self, layergroup_name, list_of_files_and_qml):
64 | self._temporal = True
65 |
66 | qgis_groups = QgsProject.instance().layerTreeRoot()
67 | if qgis_groups.findGroup(layergroup_name):
68 | self.out(f'adding layers to existing layer group "{layergroup_name}"')
69 | else:
70 | self.out(f'create layer group "{layergroup_name}"')
71 | # obtain the layer tree of the top-level group in the project
72 | root = self._iface.layerTreeCanvasBridge().rootGroup()
73 | root.addGroup(layergroup_name)
74 |
75 | # root = QgsProject.instance().layerTreeRoot() # another possibility,
76 | # but first group layers disappear in the second run(?)
77 | tree = self._iface.layerTreeCanvasBridge().rootGroup()
78 | layer_group = tree.findGroup(layergroup_name)
79 | layer_group.setExpanded(False) # collapse group after filling
80 |
81 | # for performance reason disable output for many files:
82 | saved_stdout = sys.stdout
83 | saved_stderr = sys.stderr
84 | self.out("disable output for performance reasons, loading layers...")
85 | f_devnull = open(os.devnull, 'w')
86 | sys.stdout = f_devnull
87 | sys.stderr = sys.stdout # redirect both
88 |
89 | for tif_file, qml_file in list_of_files_and_qml:
90 | bn = Path(tif_file).name
91 | raster_layer = QgsRasterLayer(str(tif_file), bn)
92 |
93 | if not raster_layer.isValid():
94 | self._show_message(Qgis.Critical, bn)
95 | continue
96 |
97 | raster_layer.setName(Path(tif_file).stem)
98 | self._insert_layer(raster_layer, qml_file, 0, layer_group)
99 | # for
100 |
101 | f_devnull.close()
102 | sys.stderr = saved_stderr
103 | sys.stdout = saved_stdout
104 | self.out("output channels restored")
105 |
106 | # TODO: collapse color definition
107 | #for layer in layer_group.findLayers():
108 | # #print(layer.name())
109 | # # filtering only raster layers
110 | # if layer.type() == QgsMapLayerType.RasterLayer: # RasterLayer?
111 | # LayerNode = root.findLayer(layer.id())
112 | # LayerNode.setExpanded(False) # hiding the bands etc.
113 |
114 | self._show_message(Qgis.Success, layer_name=f"Group {layergroup_name}", duration=5)
115 |
116 | def load_raster(self, tif_file, qml_file=None, temporal=True):
117 |
118 | self._temporal = temporal
119 |
120 | bn = Path(tif_file).name
121 |
122 | raster_layer = QgsRasterLayer(str(tif_file), bn)
123 |
124 | # this too? Lead to layer loaded twice.
125 | # QgsMapLayerRegistry.instance().addMapLayer(raster_layer)
126 |
127 | if not raster_layer.isValid():
128 | self._show_message(Qgis.Critical, bn)
129 | return
130 |
131 | """
132 | The new raster layer is ok and we will continue to work with it in the following
133 | """
134 |
135 | """
136 | --- Clean: If necessary remove existing layer ---
137 |
138 | If a raster layer with the above name already exists, it will be removed beforehand.
139 | The "legend" apparently has nothing to do with the composer legend!
140 | """
141 |
142 | # for layer in self._iface.legendInterface().layers(): # QGIS 2
143 | # for layer in self._iface.layerTreeView().selectedLayers():
144 |
145 | """ the complicated way:
146 | project = QgsProject.instance()
147 | raster_layers = [rl for rl in project.mapLayers().values() if rl.type() == 1]
148 | self.out("current project: {} raster layers found.".format(len(raster_layers)))
149 |
150 | for layer in raster_layers:
151 | #if layer.type() == QgsMapLayer.RasterLayer and layer.name() == radolan_layer_name:
152 | if layer.name() == layer_name:
153 | self.out('RasterLayer with existing name "{}" found.'.format(layer.name()))
154 | ''' Ausgabe
155 | 1 Raster
156 | 0 DEU_adm0
157 | '''
158 | # print(layerType, layer.name())
159 |
160 | print(" removing layer \"{}\"".format(layer.name()))
161 | project.removeMapLayer( layer.id() )
162 | """
163 |
164 | layer_name = Path(tif_file).stem
165 | self._layer_name = layer_name
166 | raster_layer.setName(layer_name)
167 |
168 | self._remove_layer_with_same_name(layer_name)
169 |
170 | #
171 | # Load result raster
172 | #
173 |
174 | # previous version without insert at specific position (was also ok):
175 | # self._iface.addRasterLayer(raster_to_load, path.basename(raster_to_load) )
176 |
177 | # Einfaches result_layer.setLayerName('Raster') bringt leider nichts.
178 | # Unser Rasterlayer kann zuverlässig so bestimmt werden (kann gleich noch einmal gebraucht werden):
179 | # raster_layer = self._iface.activeLayer()
180 |
181 | self._insert_layer(raster_layer, qml_file, 3)
182 |
183 | def load_vector(self, csv_file, qml_file=None):
184 | # uri = "file:///{}?encoding=UTF-8&delimiter=,&xField=LON&yField=LAT&crs=EPSG:4326".format(regnie_csv_file)
185 | # -> was working, simpler variant:
186 | # uri = "{}{}?delimiter=,&xField=LON&yField=LAT".format('file:///', regnie_csv_file)
187 | # but not on Windows - needs crs specified:
188 |
189 | uri = f"file:///{csv_file}?delimiter=,&xField=LON&yField=LAT&crs=EPSG:4326"
190 | self.out(f"uri: {uri}")
191 |
192 | # Make a vector layer:
193 | csv_layer = QgsVectorLayer(uri, csv_file.name, "delimitedtext")
194 |
195 | if not csv_layer.isValid():
196 | self._show_message(Qgis.Critical, csv_file.name)
197 | return
198 |
199 | layer_name = Path(csv_file).stem
200 | self._layer_name = layer_name
201 | csv_layer.setName(layer_name)
202 | self._remove_layer_with_same_name(layer_name)
203 | self._insert_layer(csv_layer, qml_file, 5)
204 |
205 | def _remove_layer_with_same_name(self, layer_name):
206 | layers = QgsProject.instance().mapLayersByName(layer_name)
207 |
208 | for layer in layers:
209 | self.out(f'Layer with existing name "{layer.name()}" found - removing.')
210 | QgsProject.instance().removeMapLayer(layer.id())
211 |
212 | def _insert_layer(self, layer, qml_file, duration, layer_group=None):
213 |
214 | # Build pyramids
215 | self.out("Building pyramids ...")
216 | layer = self._build_pyramids(layer)
217 |
218 | # Style layer with qml file
219 | if qml_file:
220 | self._set_qml(layer, qml_file)
221 |
222 | # Set opacity - also for black white (without QML):
223 | # Sets the opacity for the layer, where opacity is a value
224 | # between 0 (totally transparent) and 1.0 (fully opaque).
225 | opa = 0.6 # 0.6 = 40% transparency
226 |
227 | if isinstance(layer, QgsRasterLayer):
228 | layer.renderer().setOpacity(opa)
229 | else:
230 | # layer.setLayerTransparency(40) # % this method seems only be available for vector layer
231 | layer.setOpacity(opa)
232 |
233 | # Set zero values to transparent
234 | if self._no_zeros:
235 | self._set_zeroes_invisible(layer)
236 |
237 | # Set temporal settings for layer (since QGIS 3.14)
238 | if self._temporal and Qgis.QGIS_VERSION_INT >= 31400:
239 | self.out("Setting temporal settings ...")
240 | self._set_time_range(layer)
241 |
242 | # Insert layer at a certain position
243 |
244 | # Add the layer to the QGIS Map Layer Registry (the second argument must be set to False
245 | # to specify a custom position:
246 | QgsProject.instance().addMapLayer(layer, False) # first add the layer without showing it
247 |
248 | # Obtain the layer tree of the top-level group in the project.
249 | # if-else: root or layer group?
250 | if layer_group:
251 | root = layer_group
252 | else:
253 | root = self._iface.layerTreeCanvasBridge().rootGroup()
254 |
255 | # The position is a number starting from 0, with -1 an alias for the end.
256 | # index 0: uppermost, index 1: second position under the vector layer group:
257 | index = -1 if layer_group else 1
258 | root.insertChildNode(index, QgsLayerTreeLayer(layer))
259 |
260 | if not layer_group:
261 | # mark the new layer and zoom to it's extent:
262 | self._iface.setActiveLayer(layer)
263 | self._iface.zoomToActiveLayer()
264 | self._show_message(Qgis.Success, layer.name(), duration)
265 |
266 | def _set_zeroes_invisible(self, layer):
267 | self.out("setting zeroes to 100% transparency")
268 | """
269 | # Set 0 values to NODATA (= transparent):
270 | provider = active_raster_layer.dataProvider()
271 | provider.setNoDataValue(1, 0) # first one is referred to band number
272 | # -> is working
273 | """
274 | # better, conserves 0 value:
275 | tr = QgsRasterTransparency()
276 | tr.initializeTransparentPixelList(0)
277 | layer.renderer().setRasterTransparency(tr)
278 |
279 | def _set_qml(self, layer, qml_file):
280 | """
281 | @param layer: QgsRasterLayer
282 | """
283 |
284 | self.out(f"using QML file '{qml_file}'")
285 |
286 | #if layer.geometryType() == QGis.Point:
287 | layer.loadNamedStyle(str(qml_file)) # str() if Path
288 |
289 | #if hasattr(raster_layer, "setCacheImage"): # OK, 09.12.2020
290 |
291 | try:
292 | layer.setCacheImage(None)
293 | except AttributeError:
294 | pass
295 |
296 | layer.triggerRepaint() # muss aufgerufen werden, Layer bleibt sonst schwarzweiß
297 | # Das ist für die QML-Farbskala im Layerfenster:
298 | # self._iface.legendInterface().refreshLayerSymbology(active_raster_layer) # QGIS 2
299 | self._iface.layerTreeView().refreshLayerSymbology(layer.id())
300 |
301 | def _set_time_range(self, layer: QgsRasterLayer) -> None:
302 | """
303 | Sets temporal settings for layer, especially start time and end time (since QGIS 3.14).
304 |
305 | Args:
306 | layer (QgsRasterLayer): Raster layer for which to set temporal settings
307 | """
308 | time_delta = self._extract_time_delta_from_layer_name(layer.name())
309 | timestamp_end = self._extract_timestamp_from_layername(layer.name())
310 | timestamp_start = timestamp_end - time_delta
311 | self.out(f"Time range from {timestamp_start:%d.%m.%Y %H:%M} to {timestamp_end:%d.%m.%Y %H:%M}.")
312 |
313 | dt_range = QgsDateTimeRange(timestamp_start, timestamp_end)
314 | begin = dt_range.begin()
315 | begin.setTimeSpec(Qt.TimeSpec.UTC)
316 | end = dt_range.end()
317 | end.setTimeSpec(Qt.TimeSpec.UTC)
318 | dt_range = QgsDateTimeRange(begin, end)
319 | self.out(dt_range)
320 |
321 | temp_props = layer.temporalProperties()
322 | temp_props.setMode(QgsRasterLayerTemporalProperties.ModeFixedTemporalRange)
323 | #temp_props.setFixedTemporalRange(QgsDateTimeRange(timestamp_start, timestamp_end))
324 | temp_props.setFixedTemporalRange(dt_range)
325 | temp_props.setIsActive(True)
326 |
327 | def _extract_time_delta_from_layer_name(self, layername: str) -> timedelta:
328 | """
329 | Extracts the time delta from the layer name based on RADOLAN products.
330 |
331 | Args:
332 | layername (str): Name of the RADOLAN raster layer (ex. RW_20180131-0950)
333 |
334 | Returns:
335 | timedelta: Time delta object
336 | """
337 | PRODUCT_DICT = {
338 | "RY": 5,
339 | "RW": 60,
340 | "SF": 1440,
341 | }
342 | product_name = layername.split("_")[0]
343 |
344 |
345 | # TODO: better pass var "INT" from RADOLAN header instead to define again(?)
346 | try:
347 | time_delta = PRODUCT_DICT[product_name]
348 | # if product not defined, eg. "HG"product:
349 | except KeyError:
350 | time_delta = 5
351 |
352 | # time delta minus 1 minute to avoid overlapping of layers
353 | # TODO: still overlapps with next layer, but why?
354 | return timedelta(minutes=time_delta-1)
355 |
356 | def _extract_timestamp_from_layername(self, layername: str) -> datetime:
357 | """
358 | Extracts the timestamp from the layername with regular expressions.
359 |
360 | Args:
361 | layername (str): Name of the RADOLAN raster layer (ex. RW_20180131-0950)
362 |
363 | Returns:
364 | datetime: Datetime object
365 | """
366 | groups = re.findall(r"(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})", layername)
367 | groups = map(int, groups[0])
368 |
369 | return datetime(*groups)
370 |
371 | def _build_pyramids(self, layer: QgsRasterLayer) -> QgsRasterLayer:
372 | """
373 | Builds pyramids (overviews) for raster layer.
374 |
375 | Args:
376 | layer (QgsRasterLayer): Raster layer for which to build pyramids
377 |
378 | Returns:
379 | QgsRasterLayer: Raster layer with pyramids
380 | """
381 |
382 | parameters = {
383 | "INPUT": layer,
384 | "LEVELS": "2 4 8 16",
385 | "CLEAN": False,
386 | "RESAMPLING": 0,
387 | "FORMAT": 0,
388 | "OUTPUT": layer,
389 | }
390 | result = processing.run("gdal:overviews", parameters)
391 |
392 | return QgsRasterLayer(result["OUTPUT"], layer.name())
393 |
394 | # ..................................................
395 |
396 | @property
397 | def no_zeros(self):
398 | return self._no_zeros
399 |
400 | @no_zeros.setter
401 | def no_zeros(self, b):
402 | self._no_zeros = b
403 |
404 | @property
405 | def layer_name(self):
406 | return self._layer_name
407 |
--------------------------------------------------------------------------------
/classes/Model.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | from pathlib import Path
4 | import sys
5 |
6 | from configparser import ConfigParser, NoOptionError
7 | #import platform # to determine wether Windows or not
8 |
9 | from qgis.core import QgsApplication, QgsProject, QgsVectorLayer
10 | from qgis.core import QgsCoordinateReferenceSystem, QgsLayerTreeGroup
11 |
12 |
13 | from .NumpyRadolanReader import NumpyRadolanReader
14 |
15 | from . import def_products # File 'def_products.py'
16 | from . import def_projections # File 'def_projections.py'
17 | CONFIG_NAME = "config.ini"
18 |
19 | linux_tmp_root = Path("/dev/shm")
20 | linux_tmp = linux_tmp_root / "radolan2map"
21 |
22 |
23 | class Model:
24 | """
25 | Model
26 |
27 | Manages everything with paths (from config), QGIS environment,
28 | working on file system (create, delete).
29 | For the special works, such as transformations, own classes exists.
30 |
31 | Created on 23.11.2017
32 | last change: 08.11.2019
33 | @author: Weatherman
34 | """
35 |
36 |
37 | def __init__(self):
38 | print(self)
39 |
40 | #self._plugin_dir = path.abspath(path.join(path.dirname(__file__), os.pardir))
41 | #self._plugin_dir = Path(self._plugin_dir)
42 | self._plugin_dir = Path(__file__).resolve().parent.parent # classes/Model.py -> classes -> ..
43 |
44 | config_file = self._plugin_dir / CONFIG_NAME
45 |
46 | if not config_file.exists():
47 | raise FileNotFoundError(f"Config file '{config_file}'")
48 |
49 | #
50 | # Parameter
51 | #
52 | self._config_file = config_file
53 |
54 | #
55 | # Ermittelt
56 | #
57 |
58 | # Objekt, auf das wir im Folgenden immer zugreifen.
59 | # Statisch ging es leider aus welchen Gründen auch immer (war in anderen Methoden 'None').
60 | self._config = ConfigParser()
61 | self._config.read(config_file)
62 |
63 | self._product_defs = def_products.dict_titles
64 | self.out(f"{def_products.__name__}: {len(self._product_defs)} product titles loaded.")
65 |
66 | self._data_root = None # Path
67 | self._data_dir = None # Path /radolan or .../radklim or .../regnie
68 |
69 | self._l_projections = [] # EPSG numbers or projection parameters
70 | self._list_of_used_files = []
71 |
72 | self._linux_tmp = None
73 | if linux_tmp_root.exists():
74 | self.out(f"running on Linux, can use fast memory temp dir: {linux_tmp_root}")
75 | self._linux_tmp = linux_tmp
76 |
77 | """
78 | fill projections
79 | Qt-element is connected with the projection list, via same index
80 | """
81 | for number, proj_desc in self.dict_projections.items():
82 | if number > 999: # 4 digits expected
83 | projection = f"EPSG:{number}"
84 | else: # !: list type expected
85 | assert type(proj_desc) is list
86 | _, projection = proj_desc
87 |
88 | # add projection: list with epsg codes corresponds with ComboBox in 'SettingsTab.py'
89 | self._l_projections.append(projection)
90 | # for
91 |
92 |
93 | def __str__(self):
94 | return self.__class__.__name__
95 |
96 | def out(self, s, ok=True):
97 | if ok:
98 | print(f"{self}: {s}")
99 | else:
100 | print(f"{self}: {s}", file=sys.stderr)
101 |
102 | def set_data_dir(self, subdir_name):
103 | self._data_dir = self.data_root / subdir_name
104 | self.out(f"set_data_dir(): {self._data_dir}")
105 |
106 | def create_storage_folder_structure(self, use_temp_dir=None):
107 | """ affects filesystem
108 | param: use_temp_dir: if it is set, then only create the temp_dir
109 |
110 | *** REMOVE THIS SOMEDAY ***
111 | """
112 |
113 | self.out("create_storage_folder_structure()")
114 |
115 | if use_temp_dir:
116 |
117 | temp_dir = self.temp_dir
118 |
119 | if not temp_dir.exists():
120 | temp_dir.mkdir(parents=True) # mode=0o777, exist_ok=True
121 | self.out(f"temp dir created: {temp_dir}")
122 | return
123 |
124 | # Remove old content:
125 | print(" remove temp dir contents...")
126 | # If ignore_errors is set, errors are ignored; otherwise, if onerror is set, it is called to handle ...
127 | #shutil.rmtree(temp_dir)
128 | #shutil.rmtree(path, ignore_errors=True)
129 | # -> Deleting a whole directory is problematic ("in use").
130 |
131 | for f in temp_dir.glob("*"):
132 | try:
133 | f.unlink()
134 | """ Curiously an exception occurred in Windows (7) when loading data
135 | multiple times. Somehow the system does not let go of the data it touches. """
136 | except WindowsError as e: # only available on Windows
137 | self.out(f"ERROR: {e}\n try to ignore.")
138 | # for
139 | return
140 | # if use_temp_dir
141 |
142 | """
143 | temp dir maybe root was changed, so check again...
144 | data dir
145 | """
146 |
147 | for d in (self.temp_dir, self._data_dir):
148 | d.mkdir(parents=True, exist_ok=True) # mode=0o777
149 | self.out(f"makedirs(): {d}")
150 |
151 |
152 |
153 | def write_history_file(self):
154 | """
155 | hint: 'l_files' was filled in method "_init_dock()"
156 | """
157 |
158 | if not self.list_of_used_files:
159 | return
160 |
161 | _max = 9
162 |
163 | # write the _max items back:
164 | with self.history_file.open('w') as hf:
165 | for i, line in enumerate(self.list_of_used_files):
166 | if i >= _max:
167 | break
168 | hf.write(line + '\n')
169 |
170 | self.out(f"selected file stored in '{self.history_file}'")
171 |
172 |
173 | def create_default_project(self):
174 | """
175 | Load a prepared template project.
176 | see https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/loadproject.html
177 |
178 | ! Important: to make it work on other computers too,:
179 | Defined layer paths in .qgz/.qgs project file need to be defined relative
180 | (can be done manually with text editor)
181 | """
182 |
183 | self.out("create_default_project()")
184 |
185 | # Get the project instance
186 | project = QgsProject.instance() # current project
187 |
188 | # Load another project:
189 | project_file = self.default_project_file
190 | self.out(f"loading template project: {project_file}")
191 | project.read(str(project_file))
192 |
193 | """ Related to Windows / QGIS 3.10
194 | Phenomenon 1: after loading the template project,
195 | QGIS map canvas 'jumps outside' the layer extents, even though the template project
196 | was saved with map centered to Germany.
197 | Phenomenon 2: after loading the template project (actually) with crs EPSG:3035
198 | but the mouse pointer on the map showed WGS1984 coordinates.
199 | So the assumption was to try to fix that by extra setting up the project crs "from outside"
200 | to EPSG:3035.
201 | But the problem wasn't fixed with that.
202 | The crs setting below was actually inserted for Windows but we leave it here for all platforms.
203 | It can not hurt to set up the crs from outside again for the new project.
204 | """
205 | crs = QgsCoordinateReferenceSystem("EPSG:3035")
206 |
207 | if crs.isValid():
208 | self.out(f"CRS Description: {crs.description()}")
209 | self.out(f"CRS PROJ text: {crs.toProj()}")
210 | project.setCrs(crs)
211 | else:
212 | self.out("Invalid CRS!", False)
213 |
214 | return project
215 |
216 |
217 | def product_description(self, prod_id):
218 | try:
219 | return self._product_defs[prod_id]
220 | except KeyError:
221 | return "Product has not been defined yet"
222 |
223 | def title(self, prod_id, dt_date=None):
224 | """ Zugriff auf Title im Dictionary. """
225 | s_date = dt_date.strftime("%d.%m.%Y, %H:%M UTC") if dt_date else ""
226 | return f"{self.product_description(prod_id)}, {s_date}"
227 |
228 | def qml_file(self, interval_minutes, qml_fn=None):
229 | """
230 | param: interval_minutes: from header of binary file or determined by RADOLANAdder
231 | param: qml_fn: possibility to pass a QML filename directly independent from interval
232 | """
233 |
234 | self.out(f"qml_file(interval_minutes={interval_minutes})")
235 |
236 | if not qml_fn:
237 | if interval_minutes == -1: # products in RVP6-Units: RX, WX, EX
238 | qml_fn = 'rvp6units'
239 | elif interval_minutes < 60:
240 | qml_fn = '5min'
241 | elif interval_minutes < 1440:
242 | qml_fn = 'hourly'
243 | elif interval_minutes < 4320: # 3 days: 1440 * 3
244 | qml_fn = 'daily'
245 | elif interval_minutes <= 10080: # 1440 * 7
246 | qml_fn = 'daily+'
247 | elif interval_minutes <= 259200: # 1440 * (30 * 6) -> approx. 6 months
248 | qml_fn = 'monthly'
249 | # greater:
250 | else:
251 | qml_fn = 'yearly'
252 | # if
253 |
254 | qml_fn += ".qml"
255 | qml_file = self.symbology_path / qml_fn
256 |
257 | # QML file exists?
258 | if not qml_file.exists():
259 | self.out(f"qml_file(): QML file '{qml_file}' not found.", False)
260 | qml_file = None
261 |
262 | return qml_file
263 |
264 |
265 | """
266 | Properties
267 | """
268 |
269 | # Dirs:
270 |
271 | @property
272 | def profile_dir(self):
273 | return QgsApplication.qgisSettingsDirPath()
274 |
275 | @property
276 | def plugin_dir(self):
277 | return self._plugin_dir # Path
278 | @property
279 | def data_root(self):
280 | """ read from definition file
281 | raise FileNotFoundError """
282 | if self._data_root: # cached?
283 | return self._data_root
284 |
285 | with self.data_root_def_file.open() as f: # FileNotFoundError
286 | self.out(f"read 'data dir' from '{f.name}':")
287 | self._data_root = Path(f.read())
288 | print(f" {self._data_root}")
289 | return self._data_root
290 | @data_root.setter
291 | def data_root(self, d):
292 | """ update, when user writes a new path in data dir def file """
293 | self.out(f"update 'data_root': {d}")
294 | self._data_root = Path(d)
295 | @property
296 | def data_dir(self):
297 | return self._data_dir
298 | @property
299 | def temp_dir(self):
300 | # important NOT to use '_data_root' here!
301 | return self._linux_tmp if self._linux_tmp else self.data_root / 'tmp'
302 | '''
303 | @property
304 | def layout_dir(self):
305 | return self._plugin_dir / self._config.get('Paths', 'LAYOUT_PATH')
306 | '''
307 | @property
308 | def list_of_used_files(self):
309 | return self._list_of_used_files
310 | @list_of_used_files.setter
311 | def list_of_used_files(self, l):
312 | self._list_of_used_files = l
313 |
314 | @property
315 | def default_border_shape(self):
316 | """ gleich mit Check """
317 | #border_shape = path.join(self._plugin_dir, self._config.get('Paths', 'CUT_TO'))
318 | #
319 | #if not path.exists(border_shape):
320 | # self.out("default shape mask '{}' not found!".format(border_shape), False)
321 | # return None
322 |
323 | border_shape = self._plugin_dir / self._config.get('Paths', 'CUT_TO')
324 |
325 | if not border_shape.exists():
326 | self.out(f"default shape mask '{border_shape}' not found!", False)
327 | return None
328 |
329 | return str(border_shape) # convert as string, otherwise error "Posix path"
330 |
331 | @property
332 | def default_print_template(self):
333 | # verschiedene Namen wegen internem Pfad für Logo:
334 | #fn = "Druckvorlage.qpt" if not platform.system() == 'Windows' else "Druckvorlage_win.qpt"
335 |
336 | #print_template = path.join( self._config.get('Paths', 'RESSOURCE_PATH'), "Layout/{}".format(fn) )
337 | #print_template = Path(self._config.get('Paths', 'RESSOURCE_PATH')) / "Layout" / fn
338 |
339 | print_template = self._plugin_dir / self._config.get('Paths', 'print_layout')
340 |
341 | #if not path.exists(print_template):
342 | if not print_template.exists():
343 | self.out(f"default print template '{print_template}' not found!", False)
344 | return None
345 |
346 | return print_template
347 |
348 | @property
349 | def default_project_file(self):
350 | #project_file = self._config.get('Paths', 'DEFAULT_PROJECT')
351 | #project_file = path.join(self._plugin_dir, project_file)
352 | project_file = self._plugin_dir / self._config.get('Paths', 'DEFAULT_PROJECT')
353 |
354 | if not project_file.exists():
355 | self.out(f"default project '{project_file}' not found!", False)
356 | return None
357 |
358 | return project_file
359 |
360 |
361 | @property
362 | def symbology_path(self):
363 | #symb_path = self._config.get('Paths', 'STYLE_PATH')
364 | #symb_path = path.join(self._plugin_dir, symb_path)
365 | symb_path = self._plugin_dir / self._config.get('Paths', 'STYLE_PATH')
366 |
367 | #if not path.exists(symb_path):
368 | if not symb_path.exists():
369 | self.out(f"symbology path '{symb_path}' not found!", False)
370 | return None
371 |
372 | return symb_path
373 |
374 |
375 | @property
376 | def logo_path(self):
377 | default_logo_image = self._plugin_dir / "img" / 'qgis_logo.png'
378 |
379 | try:
380 | logo_image = Path(self._config.get('Paths', 'logo_image'))
381 | except NoOptionError: # use standard logo
382 | logo_image = default_logo_image
383 | else:
384 | if not logo_image.exists():
385 | self.out(f"logo image '{logo_image}' not found!", False)
386 | logo_image = default_logo_image
387 |
388 | return logo_image
389 |
390 | @property
391 | def check_file(self):
392 | return Path(self.plugin_dir) / '__check__'
393 | @property
394 | def news_file(self):
395 | return Path(self.plugin_dir) / 'news.txt'
396 | @property
397 | def settings_file(self):
398 | return Path(self.plugin_dir) / 'settings.json'
399 | @property
400 | def data_root_def_file(self):
401 | return Path(self.profile_dir) / self._config.get('Paths', 'datadir_deffile_basename')
402 | @property
403 | def history_file(self):
404 | return Path(self.profile_dir) / self._config.get('Paths', 'last_products_basename')
405 |
406 |
407 | # Projections
408 |
409 | @property
410 | def dict_projections(self):
411 | return def_projections.projs
412 |
413 | @property
414 | def projection_radolan(self):
415 | """ get CRS string for earth as sphere """
416 | _, params = def_projections.projs[0] # [ desc, params ]
417 | return params
418 |
419 | @property
420 | def projection_polara_wgs(self):
421 | """ get CRS string for earth as WGS ellipsoid """
422 | _, params = def_projections.projs[1] # [ desc, params ]
423 | return params
424 |
425 | @property
426 | def projections(self):
427 | return self._l_projections
428 |
429 |
430 |
431 | def test_product_get_id(radolan_file):
432 | """ Special function, called separately
433 | True if product is X-product (RX, WX, EX, that means coded in RVP6-units);
434 | False if not """
435 |
436 | print(f'test_product_get_id("{radolan_file}")')
437 |
438 | nrr = NumpyRadolanReader(radolan_file) # FileNotFoundError
439 | # -> file handle open
440 | header = nrr._read_radolan_header()
441 | nrr._fobj.close()
442 |
443 | prod_id = header[:2]
444 |
445 | print(f" {header}")
446 | print(f" product id: {prod_id}")
447 |
448 | return prod_id
449 |
450 |
--------------------------------------------------------------------------------
/classes/GDALProcessing.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from pathlib import Path
4 | import sys
5 | import platform # running on Windows or not
6 | import subprocess
7 |
8 | import processing
9 | from processing.algs.gdal.GdalUtils import GdalUtils # only for getting the version no.
10 | from qgis.core import QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingUtils, QgsProcessingContext
11 | from qgis.PyQt.QtCore import QSettings
12 |
13 | from osgeo import gdal #, osr # install Paket: 'python3-gdal'
14 |
15 | gdal.UseExceptions() # Enable exceptions
16 |
17 | #convert_script = 'radolanasc_to_laeatif.py' # <- .asc file
18 |
19 |
20 | class GDALProcessing:
21 | """Created on 23.11.2017
22 | @author: Weatherman"""
23 |
24 | def __init__(self, model, full_asc_filename, full_tif_filename):
25 |
26 | self.out(f"<- model, '{full_asc_filename}', '{full_tif_filename}'")
27 |
28 | #
29 | # Input data
30 | #
31 |
32 | if not model:
33 | raise OSError("'model' is None!")
34 |
35 | self._model = model
36 | self._full_asc_filename = Path(full_asc_filename)
37 | self._full_tif_filename = Path(full_tif_filename)
38 |
39 | #
40 | # determined
41 | #
42 |
43 | self._result = None # TIF-File oder TIF-Ergebnis im Speicher zur Weiterverarbeitung
44 |
45 | '''
46 | #convert_script_path = path.join(self._model.plugin_dir, convert_script)
47 | convert_script_path = path.join(path.dirname(__file__), convert_script)
48 |
49 | if not path.exists(convert_script_path):
50 | msg = "RADOLAN bin to tif convert script not found.\nSearched for: '{}'".format(convert_script_path)
51 | raise FileNotFoundError(msg)
52 | self._convert_script_path = convert_script_path
53 | '''
54 | self._convert_script_path = None # only for old method
55 |
56 | def __str__(self):
57 | return self.__class__.__name__
58 |
59 | def out(self, s, ok=True):
60 | if ok:
61 | print(f"{self}: {s}")
62 | else:
63 | print(f"{self}: {s}", file=sys.stderr)
64 |
65 | def produce_warped_tif_by_python_gdal(self, prj_src, prj_dest_epsg, shapefile=None):
66 | """Convert by OSGEO python gdal module"""
67 |
68 | self.out(f"produce_warped_tif_by_python_gdal('{prj_dest_epsg}', shapefile='{shapefile}')")
69 |
70 | #proj4_params = "+proj=stere +lat_0=90 +lat_ts=60 +lon_0=10 +k=0.93301270189"\
71 | # + " +x_0=0 +y_0=0 +a=6370040 +b=6370040 +units=m +no_defs"
72 | '''
73 | # Mit dem Parameter 'k' funktionierte der Aufruf nicht (18.07.2019).
74 | proj4_params = ('+proj=stere +lat_0=90 +lat_ts=60 +lon_0=10'
75 | '+x_0=0 +y_0=0 +a=6370040 +b=6370040'
76 | '+units=m +no_defs')
77 | '''
78 |
79 | """\
80 | A PRJ file contains a projected coordinate system.
81 | It begins with a name for the projected coordinate system.
82 | Then it describes the geographic coordinate system.
83 | Then it defines the projection and all the parameters needed for the projection.
84 | It then defines the linear units used in the projection.
85 | The final entry (AUTHORITY) is optional and describes any standard designation for this projection.
86 | Lines 2-8 define the geographic coordinate system.
87 | It begins with a name for the geographic coordinate system.
88 | Then it describes the datum.
89 | Then it defines the prime meridian used. Finally, it defines the angular units.
90 | Lines 3-5 define the geodetic datum. It begins with a name for the datum. Then it defines the spheroid.
91 | Line 4 defines the spheroid. It begins with a name for the spheroid.
92 | Then next parameter is the equatorial radius of the ellipsoid (in meters).
93 | The last parameter is the inverse flattening factor.
94 | """
95 | spatial_ref = '''\
96 | PROJCS["DWD (RADOLAN)",
97 | GEOGCS["RADOLAN Datum",
98 | DATUM["D_custom",
99 | SPHEROID["custom",6370040.0,0.0]],
100 | PRIMEM["Greenwich",0.0, AUTHORITY["EPSG","8901"]],
101 | UNIT["degree",0.017453292519943295],
102 | AXIS["Longitude",EAST],
103 | AXIS["Latitude",NORTH]],
104 | PROJECTION["polar_stereographic"],
105 | PARAMETER["latitude_of_origin",90.0],
106 | PARAMETER["central_meridian",10.0],
107 | PARAMETER["standard_parallel_1",60.0],
108 | PARAMETER["scale_factor",0.93301270189],
109 | PARAMETER["false_easting",0.0],
110 | PARAMETER["false_northing",0.0],
111 | UNIT["Meter",1.0],
112 | AXIS["X",EAST],
113 | AXIS["Y",NORTH]]'''
114 |
115 | """
116 | Beginn
117 | """
118 |
119 | # string type important! otherwise problems with gdal.Warp()!
120 | ds_in = gdal.Open(str(self._full_asc_filename))
121 | #srs = osr.SpatialReference()
122 | #srs.ImportFromWkt(spatial_ref)
123 | #ds_in.SetProjection(srs.ExportToWkt())
124 | ds_in.SetProjection(spatial_ref)
125 |
126 | """ # works:
127 | dest = osr.SpatialReference()
128 | dest.ImportFromEPSG(3035)
129 | gdal.Warp(tif_file, ds_in, srcSRS=proj4_params, dstSRS=dest)
130 | """
131 |
132 | #gdal.Warp(tif_file, ds_in, srcSRS=proj4_params, dstSRS='EPSG:3035')
133 |
134 | self.out(f"gdal.Warp from '{prj_src}' -> '{prj_dest_epsg}'")
135 | #compress_method = 'LZW'
136 | compress_method = 'DEFLATE' # lossless
137 |
138 | # from Config
139 |
140 | # with clipping:
141 | if shapefile:
142 | gdal.Warp(str(self._full_tif_filename), ds_in,
143 | cutlineDSName=f'{shapefile}', cropToCutline=True,
144 | srcSRS=prj_src, dstSRS=prj_dest_epsg,
145 | creationOptions=[f'COMPRESS={compress_method}'])
146 | # without clipping:
147 | else:
148 | gdal.Warp(str(self._full_tif_filename), ds_in,
149 | srcSRS=prj_src, dstSRS=prj_dest_epsg,
150 | creationOptions=[f'COMPRESS={compress_method}'])
151 |
152 | ds_in = None # should one do that?
153 |
154 | """
155 | following: old warp methods / scripts:
156 | """
157 |
158 | def produce_warped_tif_using_script(self):
159 | """
160 | Convert by command line
161 | """
162 |
163 | # starting with 'python3' so that it work on Windows?
164 | cmd = f'python3 {self._convert_script_path} "{self._full_asc_filename}" -o "{self._full_tif_filename.parent}"'
165 | self.out(f'running: "{cmd}"')
166 | """
167 | * To also capture standard error in the result, use stderr=subprocess.STDOUT
168 | * shell=True, wenn cmd=string. Sonst als []
169 | * Since Python 3.6 you can make check_output() return a str instead
170 | of bytes by giving it an encoding parameter: ..., encoding='UTF-8' """
171 | # Exception (wenn exit != 0) auffangen:
172 | try:
173 | out = subprocess.check_output(cmd,
174 | stderr=subprocess.STDOUT, shell=True, universal_newlines=True,
175 | encoding='UTF-8')
176 | except subprocess.CalledProcessError as e:
177 | print(e.output)
178 | else:
179 | print(out)
180 |
181 | def produce_warped_tif(self, write_result):
182 | self.out("produce_warped_tif()")
183 |
184 | """
185 | ASCII to warped TIF
186 | """
187 |
188 | # very important!
189 | if self._full_tif_filename.exists():
190 | #self.out("removing old version of warped TIF before creating a new one...")
191 | self._full_tif_filename.unlink()
192 |
193 | proj_radolan = self._model.projection_radolan
194 |
195 | """
196 | METHOD Options:
197 | 0 - near
198 | 1 - bilinear
199 | 2 - cubic
200 | 3 - cubicspline
201 | 4 - lanczos
202 | Eigentlich Default: 0 - aber man muss es angeben (QGIS 2.14 "Essen")
203 |
204 | RTYPE: default: 5 = Float32
205 |
206 | COMPRESS(GeoTIFF options. Compression type:)
207 | 0 - NONE
208 | 1 - JPEG
209 | 2 - LZW
210 | 3 - PACKBITS
211 | 4 - DEFLATE
212 | wohl kein Default, also angeben
213 | """
214 | # Standard (certain) params:
215 | params = {
216 | "INPUT": str(self._full_asc_filename),
217 | "SOURCE_SRS": proj_radolan,
218 | #"DEST_SRS": 'EPSG:3035', # QGIS 2?
219 | "TARGET_CRS": 'EPSG:3035',
220 | "METHOD": 0,
221 | "COMPRESS": 4
222 | }
223 |
224 | """
225 | useful tip:
226 | If you don't need to use the output for further elaborations, you may save it as memory layer.
227 | For doing this you only need to set the 'output' parameter as None:
228 |
229 | first = processing.runalg("gdalogr:warpreproject", { "INPUT": ... , "OUTPUT": None } )
230 |
231 | Instead, if you want to use the output, you may easily do this by giving it a name and
232 | then by calling it with getObject():
233 |
234 | second = processing.getObject( first['OUTPUT'] )
235 |
236 | -> https://gis.stackexchange.com/questions/224389/pyqgis-processing-runalg-release-input-in-windows
237 | """
238 |
239 | #params['OUTPUT'] = self._full_tif_filename if write_result else None
240 | params['OUTPUT'] = self._full_tif_filename if write_result else 'none' # in QGIS 3. Verrückt
241 |
242 |
243 | # New GdalUtils version with some additional parameters! (RAST_EXT, ...)
244 | #if version >= 2000000:
245 | # Other possibility:
246 | #>>> QgsExpressionContextUtils.globalScope().variableNames(): show all avail vars
247 | #>>> QgsExpressionContextUtils.globalScope().variable('qgis_version') # with name
248 | # out: u'2.99.0-Master'
249 | #>>> QgsExpressionContextUtils.globalScope().variable('qgis_version_no') # better
250 |
251 | """ ..............................................................
252 | HERE WE CAN SEND A GDAL CALL ADAPTED TO THE CORRESPONDING VERSION!
253 | .............................................................. """
254 |
255 | add_additional_keys = False
256 |
257 | try:
258 | version = GdalUtils.version()
259 |
260 | if platform.system() == 'Windows':
261 | system_name = f"{platform.system()} {platform.release()}"
262 | raise OSError(f"Running on '{system_name}'")
263 |
264 | #except AttributeError as ex: # ex: class GdalUtils has no attribute 'version'
265 | except AttributeError:
266 | self.out("WARN: GdalUtils.version could not determined (older version?)", False)
267 | self.out("continue without dict keys 'RAST_EXT', 'EXT_CRS'", False)
268 |
269 | except OSError as e:
270 | self.out(e, False)
271 | self.out("continue without dict keys 'RAST_EXT', 'EXT_CRS'", False)
272 |
273 | # only if try OK: section for determining the extent of input raster by creating layer datatype
274 | else:
275 | self.out(f"GdalUtils.version is {version}")
276 | # Result for
277 | # Dev-Version/Linux: 2010200
278 | # 2.18.15 Las Palmas: 2020300
279 |
280 | # For this version number it didn't work! (openSUSE Leap 42.3, QGIS version see above)
281 | if version != 2020300:
282 | add_additional_keys = True
283 |
284 | # End of try-except-else-block
285 |
286 | if add_additional_keys:
287 | # Make layer type from ASCII file to determine the mandatory 'extent':
288 | asc_layer = self._create_QGSRasterLayer_with_projection(proj_radolan) # avoid projection dialog
289 | extent = asc_layer.extent()
290 |
291 | extent_params_as_string = "%f,%f,%f,%f" \
292 | % (extent.xMinimum(), extent.xMaximum(), extent.yMinimum(), extent.yMaximum() )
293 |
294 | # insert these params later for newer GDAL versions:
295 | params['RAST_EXT'] = extent_params_as_string ### NEW! for new gdal versions.
296 | params['EXT_CRS' ] = proj_radolan ### NEW! for new gdal versions. MUST be Radolan-Proj.!!!
297 | self.out(" -> inserting new keys 'RAST_EXT', 'EXT_CRS'")
298 |
299 | # Diagnose:
300 | print("### Dict params are:\n ", params)
301 |
302 | # For overview of the parameters use in QGIS python console:
303 | # processing.alghelp("gdalogr:warpreproject")
304 | #target = processing.runalg("gdalogr:warpreproject", params ) # return: dict QGIS 2
305 | target = processing.run("gdal:warpreproject", params) # QGIS 3
306 |
307 | fallback = False
308 |
309 | # TIF shouldt be written but doesn't exists:
310 | if write_result and not self._full_tif_filename.exists():
311 | fallback = True
312 |
313 | # Normal mode: return Memory Layer:
314 | if not fallback:
315 | #self._result = processing.getObject(target["OUTPUT"]) # key of dict, QGIS 2
316 | context = QgsProcessingContext()
317 | self._result = QgsProcessingUtils.mapLayerFromString(target["OUTPUT"], context)
318 | #print("result =", self._result)
319 | return #return self._result
320 |
321 | """
322 | When warped TIF wasn't created:
323 | FALLBACK MODE without dict
324 | normally not, when dict is working
325 | The normal way is positive tested on a QGIS-dev on Linux 2.12.99.
326 | But the following fallback way need to implemented for the following
327 | configuration: also on Linux, Python-Version 2.7.12 (default, Jul 01 2016, 15:36:53) [GCC]
328 | QGIS-Version: 2.8.2 "Wien", exported
329 | """
330 | #return
331 | self._produce_warped_tif_fallback(write_result)
332 |
333 | # if success:
334 | # -> full_tif_filename is produced.
335 |
336 | def _create_QGSRasterLayer_with_projection(self, proj_radolan):
337 | """
338 | Change QGIS-Settings to avoid the assign CRS dialog to the ascii layer
339 | (appear at the point "QgsRasterLayer(...").
340 | asc_layer.setCrs(crs_radolan) is not sufficient!
341 | == Part 1 of 2 ==
342 |
343 | see
344 | https://gis.stackexchange.com/questions/27745/how-can-i-specify-the-crs-of-a-raster-layer-in-pyqgis/27765#27765
345 | """
346 | settings = QSettings()
347 | old_validation = settings.value("/Projections/defaultBehavior")
348 | settings.setValue("/Projections/defaultBehavior", "useGlobal")
349 |
350 | asc_layer = QgsRasterLayer(self._full_asc_filename, self._full_asc_filename.name)
351 |
352 | crs_radolan = QgsCoordinateReferenceSystem()
353 | crs_radolan.createFromProj4(proj_radolan)
354 | #asc_layer.setCrs( QgsCoordinateReferenceSystem(PROJ_RADOLAN_NATIVE, QgsCoordinateReferenceSystem.EpsgCrsId) )
355 | # -> funktioniert nicht :-/
356 | asc_layer.setCrs(crs_radolan)
357 |
358 | """
359 | Change QGIS-Settings to avoid the assign CRS dialog to the ascii layer.
360 | asc_layer.setCrs(crs_radolan) is not sufficient!
361 | == Part 2 of 2 ==
362 | """
363 | settings.setValue("/Projections/defaultBehavior", old_validation)
364 |
365 | return asc_layer
366 |
367 | def _produce_warped_tif_fallback(self, write_result):
368 | """ Fallback Method.
369 | Normally not used. Uses the same handed over parameters as the normal method """
370 |
371 | self.out("_produce_warped_tif_fallback()")
372 |
373 | self.out("ERROR: warped TIF wasn't produced, maybe there is a problem with"
374 | " the processing.runalg dict interface(?)", False)
375 | self.out("switching to fallback mode with handed over conventional arguments", False)
376 |
377 | """
378 | !!! here we have to fill in every param in the correct order !!!
379 |
380 | ALGORITHM: Warp (reproject)
381 |
382 | INPUT
383 | SOURCE_SRS
384 | DEST_SRS
385 | NO_DATA
386 | TR
387 | METHOD
388 | RAST_EXT NEW (in this interface not used)
389 | EXT_CRS NEW (in this interface not used)
390 | RTYPE
391 | COMPRESS
392 | JPEGCOMPRESSION
393 | ZLEVEL
394 | PREDICTOR
395 | TILED
396 | BIGTIFF
397 | TFW
398 | EXTRA
399 | OUTPUT
400 |
401 | => Defaults / meaning of values see in above not fallback warp function
402 | """
403 |
404 | result = self._full_tif_filename if write_result else None
405 |
406 | # without RAST_EXT, EXT_CRS:
407 | #processing.runalg("gdalogr:warpreproject", # QGIS 2
408 | processing.run("gdal:warpreproject", # QGIS 3
409 | self._full_asc_filename,
410 | self._model.projection_radolan, 'EPSG:3035',
411 | "", 0, 0, 5, 4, 1, 1, 1, False, 2, False, "",
412 | result)
413 |
414 | self._result = result
415 | #return result
416 |
417 | def clip_raster_by_mask(self, mask_shape):
418 |
419 | clipped_tif_name = self._full_tif_filename.name.replace('.tif', "_clipped.tif")
420 | clipped_tif_path = Path(self._model.data_dir, clipped_tif_name)
421 |
422 | self.out(f"clip_raster_by_mask('{mask_shape}')")
423 |
424 | if clipped_tif_path.exists():
425 | self.out(" removing old version of clipped TIF before creating a new one...")
426 | clipped_tif_path.unlink()
427 |
428 | # MemoryLayer as input for next step:
429 |
430 | #compress_method = 'LZW'
431 | compress_method = 'DEFLATE' # lossless
432 |
433 | #processing.runandload("gdalogr:cliprasterbymasklayer", {
434 | # 'runandload' is useable here too, but we want to have the control, set name, position etc.
435 | processing.run("gdal:cliprasterbymasklayer", {
436 | 'INPUT': self._full_tif_filename,
437 | 'MASK': mask_shape,
438 | 'OPTIONS': f'COMPRESS={compress_method}',
439 | 'OUTPUT': str(clipped_tif_path)
440 | })
441 |
442 | self.out(f" -> '{clipped_tif_path}'")
443 |
444 | self._full_tif_filename = clipped_tif_path # anpassen
445 |
446 | return clipped_tif_path
447 |
448 | ##--config GDALWARP_IGNORE_BAD_CUTLINE YES
449 |
450 | @property
451 | def tif_file(self):
452 | return self._full_tif_filename
453 |
--------------------------------------------------------------------------------