diff --git a/.github/actions/create-upload-suggestions/action.yml b/.github/actions/create-upload-suggestions/action.yml index 8ac14895db8..6a4cf1201dd 100644 --- a/.github/actions/create-upload-suggestions/action.yml +++ b/.github/actions/create-upload-suggestions/action.yml @@ -177,7 +177,7 @@ runs: echo "diff-file-name=${INPUT_DIFF_FILE_NAME}" >> "${GITHUB_OUTPUT}" env: INPUT_DIFF_FILE_NAME: ${{ steps.tool-name-safe.outputs.diff-file-name }} - - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 id: upload-diff if: >- ${{ (steps.files_changed.outputs.files_changed == 'true') && @@ -200,7 +200,7 @@ runs: echo 'Suggestions can only be added near to lines changed in this PR.' echo 'If any fixes can be added as code suggestions, they will be added shortly from another workflow.' } >> "${GITHUB_STEP_SUMMARY}" - - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 id: upload-changes if: >- ${{ always() && diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index daf1e8762b0..960941d4ede 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,7 +56,7 @@ jobs: if: ${{ matrix.language == 'c-cpp' }} - name: Initialize CodeQL - uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml @@ -81,6 +81,6 @@ jobs: run: .github/workflows/build_ubuntu-22.04.sh "${HOME}/install" - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d3104a6ed4b..9040030669c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -76,7 +76,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # v6.5.0 + uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1 with: push: true pull: true diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index da6a1a4d295..6fd2442659c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -72,7 +72,7 @@ jobs: nc_spm_full_v2alpha2.tar.gz" - name: Make HTML test report available if: ${{ always() }} - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: testreport-macOS path: testreport diff --git a/.github/workflows/macos_dependencies.txt b/.github/workflows/macos_dependencies.txt index 19edb646ac2..8f9507c15b0 100644 --- a/.github/workflows/macos_dependencies.txt +++ b/.github/workflows/macos_dependencies.txt @@ -1,39 +1,48 @@ -clang_osx-arm64 -clangxx_osx-arm64 -setuptools -python -python.app -numpy -gdal -freetype +blas cairo -matplotlib -pandoc -pillow -six -wxpython -sqlite -jpeg -libpng -libtiff -pkg-config -libiconv +clangxx_osx-arm64 +clang_osx-arm64 +cmake fftw -lapack -blas -giflib -proj +flex +freetype geos -krb5 gettext -lastools ghostscript -zstd +giflib +git +libjpeg-turbo +krb5 +lapack +lastools +libgdal-arrow-parquet +libgdal-core +libgdal-hdf4 +libgdal-hdf5 +libgdal-netcdf +libgdal-pdf +libgdal-pg +libgdal-postgisraster +libgdal-tiledb +libiconv +libjpeg-turbo +libpng +libsvm +libtiff +llvm-openmp +matplotlib +numpy<2 +pandoc pdal +pillow +pkg-config ply postgresql -# postgis>=3.1.4 -cmake -llvm-openmp -flex -git +proj +python +python.app +setuptools +six +sqlite +wxpython +zstd diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index 2fd1412d76b..ba1217620c3 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -30,7 +30,7 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: msys2/setup-msys2@5df0ca6cbf14efcd08f8d5bd5e049a3cc8e07fd2 # v2.24.0 + - uses: msys2/setup-msys2@ddf331adaebd714795f1042345e6ca57bd66cea8 # v2.24.1 with: path-type: inherit location: D:\ @@ -83,7 +83,7 @@ jobs: - name: Make HTML test report available if: ${{ always() }} - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: testreport-${{ matrix.os }} path: testreport diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 70ad658265f..17b6203ffe1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -114,7 +114,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Make python-only code coverage test report available if: ${{ !cancelled() }} - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: python-codecoverage-report-${{ matrix.os }}-${{ matrix.python-version }} path: coverage_html_report diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index ea8eb9eeac8..42c7913bdb1 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -28,15 +28,15 @@ jobs: PYTHON_VERSION: "3.10" MIN_PYTHON_VERSION: "3.8" # renovate: datasource=pypi depName=black - BLACK_VERSION: "24.4.2" + BLACK_VERSION: "24.8.0" # renovate: datasource=pypi depName=flake8 - FLAKE8_VERSION: "7.1.0" + FLAKE8_VERSION: "7.1.1" # renovate: datasource=pypi depName=pylint PYLINT_VERSION: "2.12.2" # renovate: datasource=pypi depName=bandit BANDIT_VERSION: "1.7.9" # renovate: datasource=pypi depName=ruff - RUFF_VERSION: "0.5.5" + RUFF_VERSION: "0.5.7" runs-on: ${{ matrix.os }} permissions: @@ -129,13 +129,13 @@ jobs: bandit -c pyproject.toml -iii -r . -f sarif -o bandit.sarif --exit-zero - name: Upload Bandit Scan Results - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bandit.sarif path: bandit.sarif - name: Upload SARIF File into Security Tab - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: sarif_file: bandit.sarif @@ -201,7 +201,7 @@ jobs: cp -rp dist.$ARCH/docs/html/libpython sphinx-grass - name: Make Sphinx documentation available - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: sphinx-grass path: sphinx-grass diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index e6f2899c2ce..4f70486e05e 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -147,7 +147,7 @@ jobs: - name: Make HTML test report available if: ${{ always() }} - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: testreport-${{ matrix.os }}-${{ matrix.config }}-${{ matrix.extra-include }} path: testreport diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47d0cd36bb9..2dc9dc65f35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: ) - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.5.5 + rev: v0.5.7 hooks: # Run the linter. - id: ruff @@ -49,7 +49,7 @@ repos: - id: markdownlint-fix # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black-jupyter exclude: | @@ -57,7 +57,7 @@ repos: python/libgrass_interface_generator/ ) - repo: https://github.com/pycqa/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 exclude: | diff --git a/display/d.text/test.py b/display/d.text/test.py index 6e7fe676a65..1089096d2ef 100755 --- a/display/d.text/test.py +++ b/display/d.text/test.py @@ -52,7 +52,7 @@ def text(in_text): for i in range(36): font(fonts[int(i % len(fonts))]) - size((36 - i if (i >= 9 and i <= 18 or i > 27) else i) % 9) + size((36 - i if ((i >= 9 and i <= 18) or i > 27) else i) % 9) rotate(i * 10) color(colors[i % len(colors)]) xy( diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile index ea463f5906a..dfd840263e9 100644 --- a/docker/alpine/Dockerfile +++ b/docker/alpine/Dockerfile @@ -202,6 +202,9 @@ ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 \ COPY --from=build /usr/local/bin/grass /usr/local/bin/grass COPY --from=build /usr/local/grass* /usr/local/grass/ COPY --from=build /usr/lib/gdalplugins/*_GRASS.so /usr/lib/gdalplugins/ +# Set GISBASE +ENV GISBASE /usr/local/grass + # run simple LAZ test COPY docker/testdata/simple.laz /tmp/ COPY docker/testdata/test_grass_python.py docker/testdata/test_grass_session.py docker/alpine/grass_tests.sh /scripts/ diff --git a/general/g.findfile/main.c b/general/g.findfile/main.c index 1871c5da6e1..ef75cb5ab92 100644 --- a/general/g.findfile/main.c +++ b/general/g.findfile/main.c @@ -33,6 +33,7 @@ int main(int argc, char *argv[]) struct Option *mapset_opt; struct Option *file_opt; struct Flag *n_flag, *l_flag; + size_t len; module = G_define_module(); G_add_keyword(_("general")); @@ -104,8 +105,12 @@ int main(int argc, char *argv[]) strcpy(name, file_opt->answer); G_free_tokens(map_mapset); } - else - strcpy(name, file_opt->answer); + else { + len = G_strlcpy(name, file_opt->answer, sizeof(name)); + if (len >= sizeof(name)) { + G_fatal_error(_("Name <%s> is too long"), file_opt->answer); + } + } mapset = G_find_file2(elem_opt->answer, name, search_mapset); if (mapset) { diff --git a/general/g.region/Makefile b/general/g.region/Makefile index a288c856113..9d6c871c9f4 100644 --- a/general/g.region/Makefile +++ b/general/g.region/Makefile @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../.. PGM = g.region -LIBES = $(GPROJLIB) $(VECTORLIB) $(DIG2LIB) $(RASTER3DLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PROJLIB) +LIBES = $(GPROJLIB) $(VECTORLIB) $(DIG2LIB) $(RASTER3DLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PROJLIB) $(PARSONLIB) DEPENDENCIES = $(GPROJDEP) $(VECTORDEP) $(DIG2DEP) $(RASTER3DDEP) $(RASTERDEP) $(GISDEP) EXTRA_INC = $(VECT_INC) $(PROJINC) EXTRA_CFLAGS = $(VECT_CFLAGS) diff --git a/general/g.region/g.region.html b/general/g.region/g.region.html index 4716aee2e38..85d75a058df 100644 --- a/general/g.region/g.region.html +++ b/general/g.region/g.region.html @@ -457,6 +457,59 @@

Using g.region in a shell in combination with GDAL

Here the input raster map does not have to match the project's coordinate reference system since it is reprojected on the fly. +

JSON Output

+
+g.region -p format=json
+
+
+{
+    "projection": "99 (Lambert Conformal Conic)",
+    "zone": 0,
+    "datum": "nad83",
+    "ellipsoid": "a=6378137 es=0.006694380022900787",
+    "region": {
+        "north": 320000,
+        "south": 10000,
+        "west": 120000,
+        "east": 935000,
+        "ns-res": 500,
+        "ns-res3": 1000,
+        "ew-res": 500,
+        "ew-res3": 1000
+    },
+    "top": 500,
+    "bottom": -500,
+    "tbres": 100,
+    "rows": 620,
+    "rows3": 310,
+    "cols": 1630,
+    "cols3": 815,
+    "depths": 10,
+    "cells": 1010600,
+    "cells3": 2526500
+}
+
+ +
+g.region -l format=json
+
+
+{
+    "nw_long": -78.688888505507336,
+    "nw_lat": 35.743893244701788,
+    "ne_long": -78.669097826118957,
+    "ne_lat": 35.743841072010554,
+    "se_long": -78.669158624787542,
+    "se_lat": 35.728968779193615,
+    "sw_long": -78.688945667963168,
+    "sw_lat": 35.729020942542441,
+    "center_long": -78.679022655614958,
+    "center_lat": 35.736431420327719,
+    "rows": 165,
+    "cols": 179
+}
+
+

SEE ALSO

diff --git a/general/g.region/local_proto.h b/general/g.region/local_proto.h index 781fce01e81..31c1bd21e65 100644 --- a/general/g.region/local_proto.h +++ b/general/g.region/local_proto.h @@ -13,10 +13,15 @@ #define PRINT_GMT 0x200 #define PRINT_WMS 0x400 +#include + +enum OutputFormat { PLAIN, SHELL, JSON }; + /* zoom.c */ int zoom(struct Cell_head *, const char *, const char *); /* printwindow.c */ -void print_window(struct Cell_head *, int, int); +void print_window(struct Cell_head *, int, int, enum OutputFormat, + JSON_Object *); #endif diff --git a/general/g.region/main.c b/general/g.region/main.c index 06c33a2e8ce..c36eabe4c36 100644 --- a/general/g.region/main.c +++ b/general/g.region/main.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "local_proto.h" @@ -41,6 +42,9 @@ int main(int argc, char *argv[]) char **rast_ptr, **vect_ptr; int pix; bool update_file = false; + enum OutputFormat format; + JSON_Value *root_value; + JSON_Object *root_object; struct GModule *module; struct { @@ -51,7 +55,7 @@ int main(int argc, char *argv[]) struct { struct Option *north, *south, *east, *west, *top, *bottom, *res, *nsres, *ewres, *res3, *tbres, *rows, *cols, *save, *region, *raster, - *raster3d, *align, *zoom, *vect, *grow; + *raster3d, *align, *zoom, *vect, *grow, *format; } parm; G_gisinit(argv[0]); @@ -358,6 +362,13 @@ int main(int argc, char *argv[]) parm.save->gisprompt = "new,windows,region"; parm.save->guisection = _("Effects"); + parm.format = G_define_standard_option(G_OPT_F_FORMAT); + parm.format->options = "plain,shell,json"; + parm.format->descriptions = _("plain;Plain text output;" + "shell;shell script style output;" + "json;JSON (JavaScript Object Notation);"); + parm.format->guisection = _("Print"); + G_option_required( flag.dflt, flag.savedefault, flag.print, flag.lprint, flag.eprint, flag.center, flag.gmt_style, flag.wms_style, flag.dist_res, flag.nangle, @@ -421,6 +432,24 @@ int main(int argc, char *argv[]) print_flag |= PRINT_REG; } + if (strcmp(parm.format->answer, "json") == 0) { + format = JSON; + root_value = json_value_init_object(); + if (root_value == NULL) { + G_fatal_error( + _("Failed to initialize JSON object. Out of memory?")); + } + root_object = json_object(root_value); + } + else if (strcmp(parm.format->answer, "shell") == 0 || + (print_flag & PRINT_SH)) { + format = SHELL; + print_flag |= PRINT_SH; + } + else { + format = PLAIN; + } + if (flag.dflt->answer) update_file = true; else @@ -903,7 +932,18 @@ int main(int argc, char *argv[]) } /* / flag.savedefault->answer */ if (print_flag) - print_window(&window, print_flag, flat_flag); + print_window(&window, print_flag, flat_flag, format, root_object); + + if (format == JSON) { + char *serialized_string = NULL; + serialized_string = json_serialize_to_string_pretty(root_value); + if (serialized_string == NULL) { + G_fatal_error(_("Failed to initialize pretty JSON string.")); + } + puts(serialized_string); + json_free_serialized_string(serialized_string); + json_value_free(root_value); + } exit(EXIT_SUCCESS); } diff --git a/general/g.region/printwindow.c b/general/g.region/printwindow.c index 9d8d91cb447..c36600864ff 100644 --- a/general/g.region/printwindow.c +++ b/general/g.region/printwindow.c @@ -21,7 +21,8 @@ static double get_shift(double east) return shift; } -void print_window(struct Cell_head *window, int print_flag, int flat_flag) +void print_window(struct Cell_head *window, int print_flag, int flat_flag, + enum OutputFormat format, JSON_Object *root_object) { const char *prj, *datum, *ellps; int x, width = 11; @@ -31,9 +32,13 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) char buf[50]; char *sep = "\n"; + double d_nsres, d_ewres, d_nsres3, d_ewres3, d_tbres; double ew_dist1, ew_dist2, ns_dist1, ns_dist2; double longitude, latitude; + JSON_Value *region_value; + JSON_Object *region; + if (print_flag & PRINT_SH) { x = G_projection() == PROJECTION_LL ? -1 : 0; if (flat_flag) @@ -46,6 +51,12 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) G_format_northing(window->south, south, x); G_format_easting(window->east, east, x); G_format_easting(window->west, west, x); + + d_ewres = window->ew_res; + d_ewres3 = window->ew_res3; + d_nsres = window->ns_res; + d_nsres3 = window->ns_res3; + d_tbres = window->tb_res; G_format_resolution(window->ew_res, ewres, x); G_format_resolution(window->ew_res3, ewres3, x); G_format_resolution(window->ns_res, nsres, x); @@ -81,15 +92,20 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) /* flag.dist_res */ if (print_flag & PRINT_METERS) { - sprintf(ewres, "%.8f", ((ew_dist1 + ew_dist2) / 2) / window->cols); + d_ewres = ((ew_dist1 + ew_dist2) / 2) / window->cols; + sprintf(ewres, "%.8f", d_ewres); G_trim_decimal(ewres); - sprintf(ewres3, "%.8f", ((ew_dist1 + ew_dist2) / 2) / window->cols3); + d_ewres3 = ((ew_dist1 + ew_dist2) / 2) / window->cols3; + sprintf(ewres3, "%.8f", d_ewres3); G_trim_decimal(ewres3); - sprintf(nsres, "%.8f", ((ns_dist1 + ns_dist2) / 2) / window->rows); + d_nsres = ((ns_dist1 + ns_dist2) / 2) / window->rows; + sprintf(nsres, "%.8f", d_nsres); G_trim_decimal(nsres); - sprintf(nsres3, "%.8f", ((ns_dist1 + ns_dist2) / 2) / window->rows3); + d_nsres3 = ((ns_dist1 + ns_dist2) / 2) / window->rows3; + sprintf(nsres3, "%.8f", d_nsres3); G_trim_decimal(nsres3); - sprintf(tbres, "%.8f", (window->top - window->bottom) / window->depths); + d_tbres = (window->top - window->bottom) / window->depths; + sprintf(tbres, "%.8f", d_tbres); G_trim_decimal(tbres); } @@ -99,14 +115,22 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) if (!prj) prj = "** unknown **"; - if (print_flag & PRINT_SH) { + switch (format) { + case SHELL: fprintf(stdout, "projection=%d%s", window->proj, sep); fprintf(stdout, "zone=%d%s", window->zone, sep); - } - else { + break; + case PLAIN: fprintf(stdout, "%-*s %d (%s)\n", width, "projection:", window->proj, prj); fprintf(stdout, "%-*s %d\n", width, "zone:", window->zone); + break; + case JSON: + json_object_dotset_number(root_object, "projection.code", + window->proj); + json_object_dotset_string(root_object, "projection.name", prj); + json_object_set_number(root_object, "zone", window->zone); + break; } /* don't print datum/ellipsoid in XY-Locations */ @@ -140,13 +164,22 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) } */ - if (!(print_flag & PRINT_SH)) { - fprintf(stdout, "%-*s %s\n", width, "datum:", datum); - fprintf(stdout, "%-*s %s\n", width, "ellipsoid:", ellps); + switch (format) { + case JSON: + json_object_set_string(root_object, "datum", datum); + json_object_set_string(root_object, "ellipsoid", ellps); + break; + default: + if (!(print_flag & PRINT_SH)) { + fprintf(stdout, "%-*s %s\n", width, "datum:", datum); + fprintf(stdout, "%-*s %s\n", width, "ellipsoid:", ellps); + } + break; } } - if (print_flag & PRINT_SH) { + switch (format) { + case SHELL: fprintf(stdout, "n=%s%s", north, sep); fprintf(stdout, "s=%s%s", south, sep); fprintf(stdout, "w=%s%s", west, sep); @@ -188,8 +221,8 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) (long)window->rows3 * window->cols3 * window->depths, sep); #endif - } - else { + break; + case PLAIN: fprintf(stdout, "%-*s %s\n", width, "north:", north); fprintf(stdout, "%-*s %s\n", width, "south:", south); fprintf(stdout, "%-*s %s\n", width, "west:", west); @@ -232,6 +265,42 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) fprintf(stdout, "%-*s %ld\n", width, "cells3:", (long)window->rows3 * window->cols3 * window->depths); #endif + break; + case JSON: + region_value = json_value_init_object(); + region = json_object(region_value); + json_object_set_number(region, "north", window->north); + json_object_set_number(region, "south", window->south); + json_object_set_number(region, "west", window->west); + json_object_set_number(region, "east", window->east); + json_object_set_number(region, "ns-res", d_nsres); + json_object_set_number(region, "ns-res3", d_nsres3); + json_object_set_number(region, "ew-res", d_ewres); + json_object_set_number(region, "ew-res3", d_ewres3); + json_object_set_value(root_object, "region", region_value); + json_object_set_number(root_object, "top", window->top); + json_object_set_number(root_object, "bottom", window->bottom); + json_object_set_number(root_object, "tbres", d_tbres); + json_object_set_number(root_object, "rows", window->rows); + json_object_set_number(root_object, "rows3", window->rows3); + json_object_set_number(root_object, "cols", window->cols); + json_object_set_number(root_object, "cols3", window->cols3); + json_object_set_number(root_object, "depths", window->depths); + +#ifdef HAVE_LONG_LONG_INT + json_object_set_number(root_object, "cells", + (long long)window->rows * window->cols); + json_object_set_number(root_object, "cells3", + (long long)window->rows3 * window->cols3 * + window->depths); +#else + json_object_set_number(root_object, "cells", + (long)window->rows * window->cols); + json_object_set_number(root_object, "cells3", + (long)window->rows3 * window->cols3 * + window->depths); +#endif + break; } } @@ -337,7 +406,8 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) loc = longitude; lac = latitude; - if (print_flag & PRINT_SH) { + switch (format) { + case SHELL: fprintf(stdout, "nw_long=%.8f%snw_lat=%.8f%s", lo1, sep, la1, sep); fprintf(stdout, "ne_long=%.8f%sne_lat=%.8f%s", lo2, sep, la2, @@ -348,8 +418,8 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) sep); fprintf(stdout, "center_long=%.8f%s", loc, sep); fprintf(stdout, "center_lat=%.8f%s", lac, sep); - } - else { + break; + case PLAIN: G_format_easting(lo1, buf, PROJECTION_LL); fprintf(stdout, "%-*s long: %s ", width, "north-west corner:", buf); @@ -379,16 +449,35 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) G_format_northing(lac, buf, PROJECTION_LL); fprintf(stdout, "%-*s %11s\n", width, "center latitude:", buf); + break; + case JSON: + json_object_set_number(root_object, "nw_long", lo1); + json_object_set_number(root_object, "nw_lat", la1); + json_object_set_number(root_object, "ne_long", lo2); + json_object_set_number(root_object, "ne_lat", la2); + json_object_set_number(root_object, "se_long", lo3); + json_object_set_number(root_object, "se_lat", la3); + json_object_set_number(root_object, "sw_long", lo4); + json_object_set_number(root_object, "sw_lat", la4); + json_object_set_number(root_object, "center_long", loc); + json_object_set_number(root_object, "center_lat", lac); + break; } if (!(print_flag & PRINT_REG)) { - if (print_flag & PRINT_SH) { + switch (format) { + case SHELL: fprintf(stdout, "rows=%d%s", window->rows, sep); fprintf(stdout, "cols=%d%s", window->cols, sep); - } - else { + break; + case PLAIN: fprintf(stdout, "%-*s %d\n", width, "rows:", window->rows); fprintf(stdout, "%-*s %d\n", width, "cols:", window->cols); + break; + case JSON: + json_object_set_number(root_object, "rows", window->rows); + json_object_set_number(root_object, "cols", window->cols); + break; } } } @@ -406,12 +495,13 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) /* flag.eprint */ if (print_flag & PRINT_EXTENT) { - if (print_flag & PRINT_SH) { + switch (format) { + case SHELL: fprintf(stdout, "ns_extent=%f%s", window->north - window->south, sep); fprintf(stdout, "ew_extent=%f%s", window->east - window->west, sep); - } - else { + break; + case PLAIN: if (G_projection() != PROJECTION_LL) { fprintf(stdout, "%-*s %f\n", width, "north-south extent:", window->north - window->south); @@ -426,18 +516,26 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) PROJECTION_LL); fprintf(stdout, "%-*s %s\n", width, "east-west extent:", buf); } + break; + case JSON: + json_object_set_number(root_object, "ns_extent", + window->north - window->south); + json_object_set_number(root_object, "ew_extent", + window->east - window->west); + break; } } /* flag.center */ if (print_flag & PRINT_CENTER) { - if (print_flag & PRINT_SH) { + switch (format) { + case SHELL: fprintf(stdout, "center_easting=%f%s", (window->west + window->east) / 2., sep); fprintf(stdout, "center_northing=%f%s", (window->north + window->south) / 2., sep); - } - else { + break; + case PLAIN: if (G_projection() != PROJECTION_LL) { fprintf(stdout, "%-*s %f\n", width, "center easting:", (window->west + window->east) / 2.); @@ -452,20 +550,47 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) PROJECTION_LL); fprintf(stdout, "%-*s %s\n", width, "east-west center:", buf); } + break; + case JSON: + json_object_set_number(root_object, "center_easting", + (window->west + window->east) / 2.); + json_object_set_number(root_object, "center_northing", + (window->north + window->south) / 2.); + break; } } /* flag.gmt_style */ - if (print_flag & PRINT_GMT) - fprintf(stdout, "%s/%s/%s/%s\n", west, east, south, north); + if (print_flag & PRINT_GMT) { + char gmt[120]; + switch (format) { + case JSON: + snprintf(gmt, 120, "%s/%s/%s/%s", west, east, south, north); + json_object_set_string(root_object, "GMT", gmt); + break; + default: + fprintf(stdout, "%s/%s/%s/%s\n", west, east, south, north); + break; + } + } /* flag.wms_style */ if (print_flag & PRINT_WMS) { - G_format_northing(window->north, north, -1); - G_format_northing(window->south, south, -1); - G_format_easting(window->east, east, -1); - G_format_easting(window->west, west, -1); - fprintf(stdout, "bbox=%s,%s,%s,%s%s", west, south, east, north, sep); + char wms[150]; + switch (format) { + case JSON: + snprintf(wms, 150, "bbox=%s,%s,%s,%s", west, south, east, north); + json_object_set_string(root_object, "WMS", wms); + break; + default: + G_format_northing(window->north, north, -1); + G_format_northing(window->south, south, -1); + G_format_easting(window->east, east, -1); + G_format_easting(window->west, west, -1); + fprintf(stdout, "bbox=%s,%s,%s,%s%s", west, south, east, north, + sep); + break; + } } /* flag.nangle */ @@ -540,11 +665,17 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) #endif } - if (print_flag & PRINT_SH) + switch (format) { + case SHELL: fprintf(stdout, "converge_angle=%f%s", convergence, sep); - else + break; + case PLAIN: fprintf(stdout, "%-*s %f\n", width, "convergence angle:", convergence); + break; + case JSON: + json_object_set_number(root_object, "converge_angle", convergence); + } } /* flag.bbox @@ -722,7 +853,8 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) sh_ll_e += get_shift(sh_ll_e); /* print the largest bounding box */ - if (print_flag & PRINT_SH) { + switch (format) { + case SHELL: fprintf(stdout, "ll_n=%.8f%s", sh_ll_n, sep); fprintf(stdout, "ll_s=%.8f%s", sh_ll_s, sep); fprintf(stdout, "ll_w=%.8f%s", sh_ll_w, sep); @@ -731,8 +863,8 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) fprintf(stdout, "ll_clon=%.8f%s", loc, sep); fprintf(stdout, "ll_clat=%.8f%s", (sh_ll_n + sh_ll_s) / 2., sep); - } - else { + break; + case PLAIN: G_format_northing(sh_ll_n, buf, PROJECTION_LL); fprintf(stdout, "%-*s %s\n", width, "north latitude:", buf); G_format_northing(sh_ll_s, buf, PROJECTION_LL); @@ -746,6 +878,16 @@ void print_window(struct Cell_head *window, int print_flag, int flat_flag) fprintf(stdout, "%-*s %s\n", width, "center longitude:", buf); G_format_northing((sh_ll_n + sh_ll_s) / 2., buf, PROJECTION_LL); fprintf(stdout, "%-*s %s\n", width, "center latitude:", buf); + break; + case JSON: + json_object_set_number(root_object, "ll_n", sh_ll_n); + json_object_set_number(root_object, "ll_s", sh_ll_s); + json_object_set_number(root_object, "ll_w", sh_ll_w); + json_object_set_number(root_object, "ll_e", sh_ll_e); + /* center of the largest bounding box */ + json_object_set_number(root_object, "ll_clon", loc); + json_object_set_number(root_object, "ll_clat", + (sh_ll_n + sh_ll_s) / 2.); } /*It should be calculated which number of rows and cols we have in diff --git a/general/g.region/testsuite/test_g_region.py b/general/g.region/testsuite/test_g_region.py index 7362c062f1d..a5cb0e5dc3b 100644 --- a/general/g.region/testsuite/test_g_region.py +++ b/general/g.region/testsuite/test_g_region.py @@ -3,6 +3,8 @@ @author Anna Petrasova """ +import json + from grass.gunittest.case import TestCase from grass.gunittest.gmodules import call_module import grass.script as gs @@ -46,6 +48,74 @@ def test_f_flag(self): line = call_module("g.region", flags="fglecn3", capture_stdout=True) self.assertEqual(1, len(line.splitlines())) + def test_format_json(self): + """Test json format""" + expected = { + "projection": {"code": 99, "name": "Lambert Conformal Conic"}, + "zone": 0, + "datum": "nad83", + "ellipsoid": "a=6378137 es=0.006694380022900787", + "region": { + "north": 320000, + "south": 10000, + "west": 120000, + "east": 935000, + "ns-res": 500, + "ns-res3": 1000, + "ew-res": 500, + "ew-res3": 1000, + }, + "top": 500, + "bottom": -500, + "tbres": 100, + "rows": 620, + "rows3": 310, + "cols": 1630, + "cols3": 815, + "depths": 10, + "cells": 1010600, + "cells3": 2526500, + "GMT": "120000/935000/10000/320000", + "WMS": "bbox=120000,10000,935000,320000", + "se_lat": 33.78822598716895, + "se_long": -75.48643633119754, + "sw_lat": 33.722662075471355, + "sw_long": -84.28378827453474, + "ew_extent": 815000, + "ll_clat": 35.17852919352316, + "ll_clon": -79.91588285974797, + "ll_e": -75.36388301356145, + "ll_n": 36.634396311574974, + "ll_s": 33.722662075471355, + "ll_w": -84.46788270593447, + "ne_lat": 36.58069555564894, + "ne_long": -75.36388301356145, + "ns_extent": 310000, + "nw_lat": 36.51287343603797, + "nw_long": -84.46788270593447, + "center_easting": 527500, + "center_lat": 35.23406270825775, + "center_long": -79.90206638014922, + "center_northing": 165000, + "converge_angle": -0.5206458828734528, + } + + output = call_module("g.region", flags="plectwmn3b", format="json") + output_json = json.loads(output) + + expected_ellps = expected.pop("ellipsoid").split(" ") + received_ellps = output_json.pop("ellipsoid").split(" ") + self.assertEqual(expected_ellps[0], received_ellps[0]) + self.assertAlmostEqual( + float(expected_ellps[1][3:]), float(received_ellps[1][3:]), places=6 + ) + self.assertCountEqual(list(expected.keys()), list(output_json.keys())) + for key, value in expected.items(): + if isinstance(value, float): + self.assertAlmostEqual(value, output_json[key], places=6) + else: + self.assertEqual(value, output_json[key]) + if __name__ == "__main__": from grass.gunittest.main import test diff --git a/gui/wxpython/animation/data.py b/gui/wxpython/animation/data.py index ee4e51cbef1..3799cf43ef1 100644 --- a/gui/wxpython/animation/data.py +++ b/gui/wxpython/animation/data.py @@ -89,7 +89,7 @@ def SetLayerList(self, layerList): timeseriesList.append((layer.name, layer.mapType)) self._firstStdsNameType = layer.name, layer.mapType else: - mapSeriesList.append((layer.maps)) + mapSeriesList.append(layer.maps) if not timeseriesList: self._firstStdsNameType = None, None # this throws GException diff --git a/gui/wxpython/animation/frame.py b/gui/wxpython/animation/frame.py index cec08546366..6496920a5cd 100644 --- a/gui/wxpython/animation/frame.py +++ b/gui/wxpython/animation/frame.py @@ -353,7 +353,7 @@ def OnPreferences(self, event): if not self.dialogs["preferences"]: dlg = PreferencesDialog(parent=self, giface=self._giface) self.dialogs["preferences"] = dlg - dlg.formatChanged.connect(self.controller.UpdateAnimations) + dlg.formatChanged.connect(lambda: self.controller.UpdateAnimations()) dlg.CenterOnParent() self.dialogs["preferences"].Show() diff --git a/gui/wxpython/animation/temporal_manager.py b/gui/wxpython/animation/temporal_manager.py index 2273ccfd9d6..9f9eafd41d0 100644 --- a/gui/wxpython/animation/temporal_manager.py +++ b/gui/wxpython/animation/temporal_manager.py @@ -288,26 +288,23 @@ def _getLabelsAndMaps(self, timeseries): followsPoint = True lastTimeseries = series end = None - else: - end = end - # interval data - if series: - # map exists, stop point mode - listOfMaps.append(series) - afterPoint = False - elif afterPoint: - # check point mode - if followsPoint: - # skip this one, already there - followsPoint = False - continue - else: - # append the last one (of point time) - listOfMaps.append(lastTimeseries) - end = None + elif series: + # map exists, stop point mode + listOfMaps.append(series) + afterPoint = False + elif afterPoint: + # check point mode + if followsPoint: + # skip this one, already there + followsPoint = False + continue else: - # append series which is None - listOfMaps.append(series) + # append the last one (of point time) + listOfMaps.append(lastTimeseries) + end = None + else: + # append series which is None + listOfMaps.append(series) timeLabels.append((start, end, unit)) return timeLabels, listOfMaps diff --git a/gui/wxpython/core/giface.py b/gui/wxpython/core/giface.py index f226d44eae9..864044593c0 100644 --- a/gui/wxpython/core/giface.py +++ b/gui/wxpython/core/giface.py @@ -49,8 +49,6 @@ class Layer: layer as used in lmgr. """ - pass - class LayerList: def GetSelectedLayers(self, checkedOnly=True): diff --git a/gui/wxpython/core/utils.py b/gui/wxpython/core/utils.py index 68242f04984..fe4f40c359a 100644 --- a/gui/wxpython/core/utils.py +++ b/gui/wxpython/core/utils.py @@ -3,7 +3,7 @@ @brief Misc utilities for wxGUI -(C) 2007-2015 by the GRASS Development Team +(C) 2007-2024 by the GRASS Development Team This program is free software under the GNU General Public License (>=v2). Read the file COPYING that comes with GRASS for details. @@ -23,10 +23,11 @@ from grass.script import core as grass from grass.script import task as gtask +from grass.app.runtime import get_grass_config_dir from core.gcmd import RunCommand from core.debug import Debug -from core.globalvar import ETCDIR, wxPythonPhoenix +from core.globalvar import wxPythonPhoenix def cmp(a, b): @@ -796,19 +797,8 @@ def GetFormats(writableOnly=False): def GetSettingsPath(): """Get full path to the settings directory""" - try: - verFd = open(os.path.join(ETCDIR, "VERSIONNUMBER")) - version = int(verFd.readlines()[0].split(" ")[0].split(".")[0]) - except (OSError, ValueError, TypeError, IndexError) as e: - sys.exit(_("ERROR: Unable to determine GRASS version. Details: %s") % e) - - verFd.close() - - # keep location of settings files rc and wx in sync with lib/init/grass.py - if sys.platform == "win32": - return os.path.join(os.getenv("APPDATA"), "GRASS%d" % version) - - return os.path.join(os.getenv("HOME"), ".grass%d" % version) + version_major, version_minor, _ = grass.version()["version"].split(".") + return get_grass_config_dir(version_major, version_minor, os.environ) def StoreEnvVariable(key, value=None, envFile=None): diff --git a/gui/wxpython/datacatalog/tree.py b/gui/wxpython/datacatalog/tree.py index 9ea22c1c327..faa0a6aac73 100644 --- a/gui/wxpython/datacatalog/tree.py +++ b/gui/wxpython/datacatalog/tree.py @@ -1677,9 +1677,9 @@ def DisplayLayer(self): all_names.append(name) # if self.selected_location[0].data['name'] == gisenv()['LOCATION_NAME'] and # self.selected_mapset[0]: - for ltype in names: - if names[ltype]: - self._giface.lmgr.AddMaps(list(reversed(names[ltype])), ltype, True) + for ltype, value in names.items(): + if value: + self._giface.lmgr.AddMaps(list(reversed(value)), ltype, True) if len(self._giface.GetLayerList()) == 1: # zoom to map if there is only one map layer diff --git a/gui/wxpython/dbmgr/base.py b/gui/wxpython/dbmgr/base.py index 2a58ce13e88..1c4349bdd8c 100644 --- a/gui/wxpython/dbmgr/base.py +++ b/gui/wxpython/dbmgr/base.py @@ -2962,7 +2962,6 @@ def UpdatePage(self): def OnLayerRightUp(self, event): """Layer description area, context menu""" - pass class TableListCtrl(ListCtrl, listmix.ListCtrlAutoWidthMixin): @@ -3019,7 +3018,7 @@ def Populate(self, update=False): str(self.table[column]["type"]), int(self.table[column]["length"]), ) - i = i + 1 + i += 1 self.SendSizeEvent() diff --git a/gui/wxpython/dbmgr/dialogs.py b/gui/wxpython/dbmgr/dialogs.py index c5b63d0fe13..757fde03cbf 100644 --- a/gui/wxpython/dbmgr/dialogs.py +++ b/gui/wxpython/dbmgr/dialogs.py @@ -189,7 +189,6 @@ def __init__( def OnSQLStatement(self, event): """Update SQL statement""" - pass def IsFound(self): """Check for status diff --git a/gui/wxpython/gcp/manager.py b/gui/wxpython/gcp/manager.py index afc3b239037..98323112972 100644 --- a/gui/wxpython/gcp/manager.py +++ b/gui/wxpython/gcp/manager.py @@ -1243,7 +1243,6 @@ def __del__(self): """Disable GCP manager mode""" # leaving the method here but was used only to delete gcpmanagement # from layer manager which is now not needed - pass def CreateGCPList(self): """Create GCP List Control""" @@ -2377,7 +2376,6 @@ def OnIdle(self, event): self.resize = False elif self.resize: event.RequestMore() - pass class GCPDisplay(FrameMixin, GCPPanel): diff --git a/gui/wxpython/gmodeler/dialogs.py b/gui/wxpython/gmodeler/dialogs.py index 10729b83289..0b4038b7b50 100644 --- a/gui/wxpython/gmodeler/dialogs.py +++ b/gui/wxpython/gmodeler/dialogs.py @@ -206,7 +206,9 @@ def __init__( parent=self, giface=giface, menuModel=menuModel.GetModel() ) self.cmd_prompt.promptRunCmd.connect(self.OnCommand) - self.cmd_prompt.commandSelected.connect(self.label.SetValue) + self.cmd_prompt.commandSelected.connect( + lambda command: self.label.SetValue(command) + ) self.search = SearchModuleWidget( parent=self.panel, model=menuModel.GetModel(), showTip=True ) @@ -542,7 +544,6 @@ def __init__( def _layout(self): """Do layout (virtual method)""" - pass def GetCondition(self): """Get loop condition""" @@ -775,7 +776,6 @@ def OnBeginEdit(self, event): def OnEndEdit(self, event): """Finish editing of item""" - pass def GetListCtrl(self): """Used by ColumnSorterMixin""" diff --git a/gui/wxpython/gmodeler/model.py b/gui/wxpython/gmodeler/model.py index 4596e0b04eb..1d0e508d7c5 100644 --- a/gui/wxpython/gmodeler/model.py +++ b/gui/wxpython/gmodeler/model.py @@ -1390,7 +1390,6 @@ def _defineShape(self, width, height, x, y): :param width, height: dimension of the shape :param x, y: position of the shape """ - pass def IsIntermediate(self): """Checks if data item is intermediate""" @@ -2257,7 +2256,7 @@ def _processConditions(self): pos, size = self._getDim(node) text = self._filterValue(self._getNodeText(node, "condition")).strip() aid = {"if": [], "else": []} - for b in aid.keys(): + for b in aid.keys(): # noqa: PLC0206 bnode = node.find(b) if bnode is None: continue @@ -3488,7 +3487,7 @@ def cleanup(): r""" %s("g.remove", flags="f", type="vector", name=%s) """ - % (run_command, ",".join(('"' + x + '"' for x in vect))) + % (run_command, ",".join(f'"{x}"' for x in vect)) ) if rast3d: self.fd.write( diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 8179f07e244..6d6bff45387 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -156,7 +156,9 @@ def __init__( self.goutput = GConsoleWindow( parent=self, giface=giface, gconsole=self._gconsole ) - self.goutput.showNotification.connect(self.SetStatusText) + self.goutput.showNotification.connect( + lambda message: self.SetStatusText(message) + ) # here events are binded twice self._gconsole.Bind( diff --git a/gui/wxpython/gui_core/dialogs.py b/gui/wxpython/gui_core/dialogs.py index 05bb71ed7fa..c31939312a7 100644 --- a/gui/wxpython/gui_core/dialogs.py +++ b/gui/wxpython/gui_core/dialogs.py @@ -1471,13 +1471,11 @@ def _modelerDSeries(self): """Method used only by MapLayersDialogForModeler, for other subclasses does nothing. """ - pass def _addApplyButton(self): """Method used only by MapLayersDialog, for other subclasses does nothing. """ - pass def _fullyQualifiedNames(self): """Adds CheckBox which determines is fully qualified names are retuned.""" diff --git a/gui/wxpython/gui_core/forms.py b/gui/wxpython/gui_core/forms.py index 89aa24c5844..848e8e94c4e 100644 --- a/gui/wxpython/gui_core/forms.py +++ b/gui/wxpython/gui_core/forms.py @@ -564,7 +564,9 @@ def __init__( self._gconsole.mapCreated.connect(self.OnMapCreated) self.goutput = self.notebookpanel.goutput if self.goutput: - self.goutput.showNotification.connect(self.SetStatusText) + self.goutput.showNotification.connect( + lambda message: self.SetStatusText(message) + ) self.notebookpanel.OnUpdateValues = self.updateValuesHook guisizer.Add(self.notebookpanel, proportion=1, flag=wx.EXPAND) @@ -2850,7 +2852,6 @@ def OnUpdateValues(self, event=None): needed. It's a hook, actually. Beware of what is 'self' in the method def, though. It will be called with no arguments. """ - pass def OnCheckBoxMulti(self, event): """Fill the values as a ','-separated string according to diff --git a/gui/wxpython/gui_core/gselect.py b/gui/wxpython/gui_core/gselect.py index 215a7bba23d..7dc51b75935 100644 --- a/gui/wxpython/gui_core/gselect.py +++ b/gui/wxpython/gui_core/gselect.py @@ -2903,7 +2903,7 @@ def _draw(self, delay): coords = self._getCoords() if coords is not None: for i in range(len(coords) // 2): - i = i * 2 + i *= 2 self.pointsToDraw.AddItem(coords=(coords[i], coords[i + 1])) self._giface.updateMap.emit(render=False, renderVector=False, delay=delay) diff --git a/gui/wxpython/gui_core/menu.py b/gui/wxpython/gui_core/menu.py index f435fe4c9f8..71d6d153f8b 100644 --- a/gui/wxpython/gui_core/menu.py +++ b/gui/wxpython/gui_core/menu.py @@ -227,7 +227,7 @@ def __init__(self, parent, handlerObj, giface, model, id=wx.ID_ANY, **kwargs): self._btnAdvancedSearch.Bind(wx.EVT_BUTTON, lambda evt: self.AdvancedSearch()) self._tree.selectionChanged.connect(self.OnItemSelected) - self._tree.itemActivated.connect(self.Run) + self._tree.itemActivated.connect(lambda node: self.Run(node)) self._layout() diff --git a/gui/wxpython/gui_core/prompt.py b/gui/wxpython/gui_core/prompt.py index 13b4d0955b7..6ac9219f827 100644 --- a/gui/wxpython/gui_core/prompt.py +++ b/gui/wxpython/gui_core/prompt.py @@ -167,9 +167,11 @@ def __init__(self, parent, giface, menuModel, margin=False): self._loadHistory() if giface: giface.currentMapsetChanged.connect(self._loadHistory) - giface.entryToHistoryAdded.connect(self._addEntryToCmdHistoryBuffer) + giface.entryToHistoryAdded.connect( + lambda entry: self._addEntryToCmdHistoryBuffer(entry) + ) giface.entryFromHistoryRemoved.connect( - self._removeEntryFromCmdHistoryBuffer + lambda index: self._removeEntryFromCmdHistoryBuffer(index) ) # # bindings @@ -431,7 +433,7 @@ def GetWordLeft(self, withDelimiter=False, ignoredDelimiter=None): else: delimiter = char parts.append(delimiter + textLeft.rpartition(char)[2]) - return min(parts, key=len) + return min(parts, key=lambda x: len(x)) def ShowList(self): """Show sorted auto-completion list if it is not empty""" @@ -475,9 +477,9 @@ def OnKeyPressed(self, event): # move through command history list index values if event.GetKeyCode() == wx.WXK_UP: - self.cmdindex = self.cmdindex - 1 + self.cmdindex -= 1 if event.GetKeyCode() == wx.WXK_DOWN: - self.cmdindex = self.cmdindex + 1 + self.cmdindex += 1 self.cmdindex = max(self.cmdindex, 0) self.cmdindex = min(self.cmdindex, len(self.cmdbuffer) - 1) diff --git a/gui/wxpython/gui_core/pystc.py b/gui/wxpython/gui_core/pystc.py index 00db4d8a0c8..a1dfc543320 100644 --- a/gui/wxpython/gui_core/pystc.py +++ b/gui/wxpython/gui_core/pystc.py @@ -353,7 +353,7 @@ def FoldAll(self): if expanding: self.SetFoldExpanded(lineNum, True) lineNum = self.Expand(lineNum, True) - lineNum = lineNum - 1 + lineNum -= 1 else: lastChild = self.GetLastChild(lineNum, -1) self.SetFoldExpanded(lineNum, False) @@ -361,11 +361,11 @@ def FoldAll(self): if lastChild > lineNum: self.HideLines(lineNum + 1, lastChild) - lineNum = lineNum + 1 + lineNum += 1 def Expand(self, line, doExpand, force=False, visLevels=0, level=-1): lastChild = self.GetLastChild(line, level) - line = line + 1 + line += 1 while line <= lastChild: if force: @@ -392,6 +392,6 @@ def Expand(self, line, doExpand, force=False, visLevels=0, level=-1): else: line = self.Expand(line, False, force, visLevels - 1) else: - line = line + 1 + line += 1 return line diff --git a/gui/wxpython/gui_core/query.py b/gui/wxpython/gui_core/query.py index 0dfd21d8765..6250095cb29 100644 --- a/gui/wxpython/gui_core/query.py +++ b/gui/wxpython/gui_core/query.py @@ -148,7 +148,9 @@ def ShowContextMenu(self, node): id = NewId() ids.append(id) self.Bind( - wx.EVT_MENU, lambda evt, t=text[1], id=id: self._copyText(t), id=id + wx.EVT_MENU, + lambda evt, t=text[1], id=id: self._copyText(t), # noqa: A006 + id=id, ) menu.Append(id, text[0]) diff --git a/gui/wxpython/gui_core/simplelmgr.py b/gui/wxpython/gui_core/simplelmgr.py index 9f03a49396e..eb0c0a00688 100644 --- a/gui/wxpython/gui_core/simplelmgr.py +++ b/gui/wxpython/gui_core/simplelmgr.py @@ -159,7 +159,11 @@ def OnContextMenu(self, event): ] for label, text in zip(labels, texts): id = NewId() - self.Bind(wx.EVT_MENU, lambda evt, t=text, id=id: self._copyText(t), id=id) + self.Bind( + wx.EVT_MENU, + lambda evt, t=text, id=id: self._copyText(t), # noqa: A006 + id=id, + ) menu.Append(id, label) diff --git a/gui/wxpython/gui_core/widgets.py b/gui/wxpython/gui_core/widgets.py index 3196ff173db..035c70e6fbd 100644 --- a/gui/wxpython/gui_core/widgets.py +++ b/gui/wxpython/gui_core/widgets.py @@ -80,7 +80,7 @@ from wx.lib.buttons import GenBitmapTextButton as BitmapTextButton if wxPythonPhoenix: - from wx import Validator as Validator + from wx import Validator else: from wx import PyValidator as Validator @@ -348,7 +348,6 @@ def RemoveNBPage(self, page): def SetPageImage(self, page, index): """Does nothing because we don't want images for this style""" - pass def __getattr__(self, name): return getattr(self.controller, name) @@ -1335,7 +1334,7 @@ def _searchModule(self, keys, value): nodes.update(self._model.SearchNodes(key=key, value=value)) nodes = list(nodes) - nodes.sort(key=self._model.GetIndexOfNode) + nodes.sort(key=lambda node: self._model.GetIndexOfNode(node)) self._results = nodes self._resultIndex = -1 return sorted([node.data["command"] for node in nodes if node.data["command"]]) @@ -1799,7 +1798,7 @@ def OnLeftDown(self, event): colLocs = [0] loc = 0 for n in range(self.GetColumnCount()): - loc = loc + self.GetColumnWidth(n) + loc += self.GetColumnWidth(n) colLocs.append(loc) col = bisect(colLocs, x + self.GetScrollPos(wx.HORIZONTAL)) - 1 diff --git a/gui/wxpython/history/tree.py b/gui/wxpython/history/tree.py index 98ab8eb0994..9119473d9ff 100644 --- a/gui/wxpython/history/tree.py +++ b/gui/wxpython/history/tree.py @@ -137,8 +137,12 @@ def __init__( self.runIgnoredCmdPattern = Signal("HistoryBrowserTree.runIgnoredCmdPattern") self._giface.currentMapsetChanged.connect(self.UpdateHistoryModelFromScratch) - self._giface.entryToHistoryAdded.connect(self.InsertCommand) - self._giface.entryInHistoryUpdated.connect(self.UpdateCommand) + self._giface.entryToHistoryAdded.connect( + lambda entry: self.InsertCommand(entry) + ) + self._giface.entryInHistoryUpdated.connect( + lambda entry: self.UpdateCommand(entry) + ) self.SetToolTip(_("Double-click to open the tool")) self.selectionChanged.connect(self.OnItemSelected) diff --git a/gui/wxpython/iclass/frame.py b/gui/wxpython/iclass/frame.py index c6a401ad4d6..52af10d85e0 100644 --- a/gui/wxpython/iclass/frame.py +++ b/gui/wxpython/iclass/frame.py @@ -139,7 +139,9 @@ def __init__( # TODO: for vdigit: it does nothing here because areas do not produce # this info self.firstMapWindow.digitizingInfo.connect( - self.statusbarManager.statusbarItems["coordinates"].SetAdditionalInfo + lambda text: self.statusbarManager.statusbarItems[ + "coordinates" + ].SetAdditionalInfo(text) ) self.firstMapWindow.digitizingInfoUnavailable.connect( lambda: self.statusbarManager.statusbarItems[ diff --git a/gui/wxpython/image2target/ii2t_manager.py b/gui/wxpython/image2target/ii2t_manager.py index b4bc3d60f97..968e6c50b7d 100644 --- a/gui/wxpython/image2target/ii2t_manager.py +++ b/gui/wxpython/image2target/ii2t_manager.py @@ -1228,7 +1228,6 @@ def __del__(self): """Disable GCP manager mode""" # leaving the method here but was used only to delete gcpmanagement # from layer manager which is now not needed - pass def CreateGCPList(self): """Create GCP List Control""" @@ -2316,7 +2315,6 @@ def OnIdle(self, event): self.resize = False elif self.resize: event.RequestMore() - pass class GCPDisplay(FrameMixin, GCPPanel): diff --git a/gui/wxpython/iscatt/core_c.py b/gui/wxpython/iscatt/core_c.py index cda449b0dd5..a2a3d5da9da 100644 --- a/gui/wxpython/iscatt/core_c.py +++ b/gui/wxpython/iscatt/core_c.py @@ -58,7 +58,7 @@ def ApplyColormap(vals, vals_mask, colmap, out_vals): colmap_p = colmap.ctypes.data_as(c_uint8_p) out_vals_p = out_vals.ctypes.data_as(c_uint8_p) - vals_size = vals.reshape((-1)).shape[0] + vals_size = vals.reshape(-1).shape[0] I_apply_colormap(vals_p, vals_mask_p, vals_size, colmap_p, out_vals_p) diff --git a/gui/wxpython/iscatt/iscatt_core.py b/gui/wxpython/iscatt/iscatt_core.py index 2eaa463940d..97da83415e8 100644 --- a/gui/wxpython/iscatt/iscatt_core.py +++ b/gui/wxpython/iscatt/iscatt_core.py @@ -832,7 +832,6 @@ def GetRasterInfo(rast): if k == "datatype": if v != "CELL": return None - pass elif k in {"rows", "cols", "cells", "min", "max"}: v = int(v) else: diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index ddb91789508..de06305086f 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -389,7 +389,9 @@ def _createNotebook(self): def _createDataCatalog(self, parent): """Initialize Data Catalog widget""" self.datacatalog = DataCatalog(parent=parent, giface=self._giface) - self.datacatalog.showNotification.connect(self.SetStatusText) + self.datacatalog.showNotification.connect( + lambda message: self.SetStatusText(message) + ) def _createDisplay(self, parent): """Initialize Display widget""" @@ -410,7 +412,9 @@ def _createSearchModule(self, parent): giface=self._giface, model=self._moduleTreeBuilder.GetModel(), ) - self.search.showNotification.connect(self.SetStatusText) + self.search.showNotification.connect( + lambda message: self.SetStatusText(message) + ) else: self.search = None @@ -430,7 +434,9 @@ def _createConsole(self, parent): menuModel=self._moduleTreeBuilder.GetModel(), gcstyle=GC_PROMPT, ) - self.goutput.showNotification.connect(self.SetStatusText) + self.goutput.showNotification.connect( + lambda message: self.SetStatusText(message) + ) self._gconsole.mapCreated.connect(self.OnMapCreated) self._gconsole.Bind( @@ -443,7 +449,9 @@ def _createHistoryBrowser(self, parent): """Initialize history browser widget""" if not UserSettings.Get(group="manager", key="hideTabs", subkey="history"): self.history = HistoryBrowser(parent=parent, giface=self._giface) - self.history.showNotification.connect(self.SetStatusText) + self.history.showNotification.connect( + lambda message: self.SetStatusText(message) + ) self.history.runIgnoredCmdPattern.connect( lambda cmd: self.RunSpecialCmd(command=cmd), ) @@ -629,7 +637,9 @@ def _addPagesToNotebook(self): # add 'console' widget to main notebook page and add connect switch page signal self.notebook.AddPage(page=self.goutput, text=_("Console"), name="output") - self.goutput.contentChanged.connect(self._switchPage) + self.goutput.contentChanged.connect( + lambda notification: self._switchPage(notification) + ) # add 'history module' widget to main notebook page if self.history: diff --git a/gui/wxpython/lmgr/layertree.py b/gui/wxpython/lmgr/layertree.py index e2204555415..4779a40db9b 100644 --- a/gui/wxpython/lmgr/layertree.py +++ b/gui/wxpython/lmgr/layertree.py @@ -1248,7 +1248,7 @@ def OnPopupGroupOpacityLevel(self, event): lambda value: self.ChangeGroupLayerOpacity(layer=child, value=value) ) # Apply button - dlg.applyOpacity.connect(self._recalculateLayerButtonPosition) + dlg.applyOpacity.connect(lambda: self._recalculateLayerButtonPosition()) dlg.CentreOnParent() if dlg.ShowModal() == wx.ID_OK: @@ -1288,7 +1288,7 @@ def OnPopupOpacityLevel(self, event): ) ) # Apply button - dlg.applyOpacity.connect(self._recalculateLayerButtonPosition) + dlg.applyOpacity.connect(lambda: self._recalculateLayerButtonPosition()) dlg.CentreOnParent() if dlg.ShowModal() == wx.ID_OK: diff --git a/gui/wxpython/location_wizard/dialogs.py b/gui/wxpython/location_wizard/dialogs.py index aa3954dab25..16325ddfbe1 100644 --- a/gui/wxpython/location_wizard/dialogs.py +++ b/gui/wxpython/location_wizard/dialogs.py @@ -702,9 +702,9 @@ def __init__( height += h width = max(width, w) - height = height + 5 + height += 5 height = min(height, 400) - width = width + 5 + width += 5 width = min(width, 400) # @@ -746,12 +746,12 @@ def __init__( def ClickTrans(self, event): """Get the number of the datum transform to use in g.proj""" self.transnum = event.GetSelection() - self.transnum = self.transnum - 1 + self.transnum -= 1 def GetTransform(self): """Get the number of the datum transform to use in g.proj""" self.transnum = self.translist.GetSelection() - self.transnum = self.transnum - 1 + self.transnum -= 1 return self.transnum diff --git a/gui/wxpython/location_wizard/wizard.py b/gui/wxpython/location_wizard/wizard.py index 5a206fafa52..155346b3467 100644 --- a/gui/wxpython/location_wizard/wizard.py +++ b/gui/wxpython/location_wizard/wizard.py @@ -2332,7 +2332,7 @@ def OnEnterPage(self, event): finishButton = wx.FindWindowById(wx.ID_FORWARD) if ret == 0: if datum != "": - projlabel = projlabel + "+datum=%s" % datum + projlabel += "+datum=%s" % datum self.lproj4string.SetLabel(projlabel.replace(" +", os.linesep + "+")) finishButton.Enable(True) else: diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 6b9a9be640f..facb3f8195a 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -331,7 +331,9 @@ def _createMainNotebook(self): def _createDataCatalog(self, parent): """Initialize Data Catalog widget""" self.datacatalog = DataCatalog(parent=parent, giface=self._giface) - self.datacatalog.showNotification.connect(self.SetStatusText) + self.datacatalog.showNotification.connect( + lambda message: self.SetStatusText(message) + ) def _createDisplay(self, parent): """Initialize Display widget""" @@ -353,7 +355,9 @@ def _createSearchModule(self, parent): giface=self._giface, model=self._moduleTreeBuilder.GetModel(), ) - self.search.showNotification.connect(self.SetStatusText) + self.search.showNotification.connect( + lambda message: self.SetStatusText(message) + ) else: self.search = None @@ -373,8 +377,12 @@ def _createConsole(self, parent): menuModel=self._moduleTreeBuilder.GetModel(), gcstyle=GC_PROMPT, ) - self.goutput.showNotification.connect(self.SetStatusText) - self.goutput.contentChanged.connect(self._focusPage) + self.goutput.showNotification.connect( + lambda message: self.SetStatusText(message) + ) + self.goutput.contentChanged.connect( + lambda notification: self._focusPage(notification) + ) self._gconsole.mapCreated.connect(self.OnMapCreated) self._gconsole.Bind( @@ -387,7 +395,9 @@ def _createHistoryBrowser(self, parent): """Initialize history browser widget""" if not UserSettings.Get(group="manager", key="hideTabs", subkey="history"): self.history = HistoryBrowser(parent=parent, giface=self._giface) - self.history.showNotification.connect(self.SetStatusText) + self.history.showNotification.connect( + lambda message: self.SetStatusText(message) + ) self.history.runIgnoredCmdPattern.connect( lambda cmd: self.RunSpecialCmd(command=cmd), ) diff --git a/gui/wxpython/mapdisp/frame.py b/gui/wxpython/mapdisp/frame.py index 96f147c3cb0..8cb8a51d407 100644 --- a/gui/wxpython/mapdisp/frame.py +++ b/gui/wxpython/mapdisp/frame.py @@ -324,7 +324,9 @@ def _addToolbarVDigit(self): ) self._setUpMapWindow(self.MapWindowVDigit) self.MapWindowVDigit.digitizingInfo.connect( - self.statusbarManager.statusbarItems["coordinates"].SetAdditionalInfo + lambda text: self.statusbarManager.statusbarItems[ + "coordinates" + ].SetAdditionalInfo(text) ) self.MapWindowVDigit.digitizingInfoUnavailable.connect( lambda: self.statusbarManager.statusbarItems[ @@ -364,7 +366,9 @@ def _addToolbarVDigit(self): def openATM(selection): self._layerManager.OnShowAttributeTable(None, selection=selection) - self.toolbars["vdigit"].openATM.connect(openATM) + self.toolbars["vdigit"].openATM.connect( + lambda selection: openATM(selection) + ) self.Map.layerAdded.connect(self._updateVDigitLayers) self.MapWindowVDigit.SetToolbar(self.toolbars["vdigit"]) @@ -1243,7 +1247,9 @@ def _onMeasure(self, controller): self.measureController = controller(self._giface, mapWindow=self.GetMapWindow()) # assure that the mode is ended and lines are cleared whenever other # tool is selected - self._toolSwitcher.toggleToolChanged.connect(self.measureController.Stop) + self._toolSwitcher.toggleToolChanged.connect( + lambda: self.measureController.Stop() + ) self.measureController.Start() def OnProfile(self, event): diff --git a/gui/wxpython/mapdisp/gprint.py b/gui/wxpython/mapdisp/gprint.py index b72a265f035..3172f58eab5 100644 --- a/gui/wxpython/mapdisp/gprint.py +++ b/gui/wxpython/mapdisp/gprint.py @@ -59,8 +59,8 @@ def OnPrintPage(self, page): marginY = 10 # Add the margin to the graphic size - maxX = maxX + (2 * marginX) - maxY = maxY + (2 * marginY) + maxX += 2 * marginX + maxY += 2 * marginY # Get the size of the DC in pixels (w, h) = dc.GetSizeTuple() diff --git a/gui/wxpython/mapdisp/main.py b/gui/wxpython/mapdisp/main.py index e1774ed472b..5bef25ad128 100644 --- a/gui/wxpython/mapdisp/main.py +++ b/gui/wxpython/mapdisp/main.py @@ -579,8 +579,8 @@ def CreateMapDisplay(self, name, decorations=True): ), ) - self.Map.saveToFile.connect(self.mapDisplay.DOutFile) - self.Map.dToRast.connect(self.mapDisplay.DToRast) + self.Map.saveToFile.connect(lambda cmd: self.mapDisplay.DOutFile(cmd)) + self.Map.dToRast.connect(lambda cmd: self.mapDisplay.DToRast(cmd)) self.Map.query.connect( lambda ltype, maps: self.mapDisplay.SetQueryLayersAndActivate( ltype=ltype, maps=maps diff --git a/gui/wxpython/mapdisp/properties.py b/gui/wxpython/mapdisp/properties.py index 92b9b38474b..5fd9fdfe031 100644 --- a/gui/wxpython/mapdisp/properties.py +++ b/gui/wxpython/mapdisp/properties.py @@ -44,7 +44,6 @@ def mapWindowProperty(self, value): def mapWindowPropertyChanged(self): """Returns signal from MapWindowProperties.""" - pass def _setValue(self, value): self.widget.SetValue(value) diff --git a/gui/wxpython/mapdisp/toolbars.py b/gui/wxpython/mapdisp/toolbars.py index 3589c7fd5de..4eb49846d4f 100644 --- a/gui/wxpython/mapdisp/toolbars.py +++ b/gui/wxpython/mapdisp/toolbars.py @@ -252,7 +252,7 @@ def _toolbarData(self): ), ) if self.parent.IsDockable(): - data = data + ( + data += ( ( ("docking", BaseIcons["docking"].label), BaseIcons["docking"], diff --git a/gui/wxpython/mapswipe/frame.py b/gui/wxpython/mapswipe/frame.py index 14c1fd91e28..a7dd0df8f56 100644 --- a/gui/wxpython/mapswipe/frame.py +++ b/gui/wxpython/mapswipe/frame.py @@ -85,8 +85,8 @@ def __init__( self.secondMapWindow.mapQueried.connect(self.Query) # bind tracking cursosr to mirror it - self.firstMapWindow.Bind(wx.EVT_MOTION, self.TrackCursor) - self.secondMapWindow.Bind(wx.EVT_MOTION, self.TrackCursor) + self.firstMapWindow.Bind(wx.EVT_MOTION, lambda evt: self.TrackCursor(evt)) + self.secondMapWindow.Bind(wx.EVT_MOTION, lambda evt: self.TrackCursor(evt)) self.MapWindow = self.firstMapWindow # current by default self.firstMapWindow.zoomhistory = self.secondMapWindow.zoomhistory @@ -659,7 +659,6 @@ def OnAddText(self, event): So far not implemented. """ - pass def SetViewMode(self, mode): """Sets view mode. @@ -784,7 +783,9 @@ def Query(self, x, y): else: self._queryDialog = QueryDialog(parent=self, data=result) self._queryDialog.Bind(wx.EVT_CLOSE, self._oncloseQueryDialog) - self._queryDialog.redirectOutput.connect(self._giface.WriteLog) + self._queryDialog.redirectOutput.connect( + lambda output: self._giface.WriteLog(output) + ) self._queryDialog.Show() def _oncloseQueryDialog(self, event): diff --git a/gui/wxpython/mapswipe/toolbars.py b/gui/wxpython/mapswipe/toolbars.py index 135a04567ae..943eddaa1ec 100644 --- a/gui/wxpython/mapswipe/toolbars.py +++ b/gui/wxpython/mapswipe/toolbars.py @@ -123,7 +123,6 @@ def SetActiveMap(self, index): """Set currently selected map. Unused, needed because of DoubleMapPanel API. """ - pass class SwipeMainToolbar(BaseToolbar): diff --git a/gui/wxpython/mapwin/analysis.py b/gui/wxpython/mapwin/analysis.py index 907817335f6..490a9d7e866 100644 --- a/gui/wxpython/mapwin/analysis.py +++ b/gui/wxpython/mapwin/analysis.py @@ -297,7 +297,7 @@ def MeasureDist(self, beginpt, endpt): # the mathematical theta convention (CCW from +x axis) # angle = 90 - angle if angle < 0: - angle = 360 + angle + angle += 360 mstring = "%s = %s %s\n%s = %s %s\n%s = %d %s\n%s" % ( _("segment"), diff --git a/gui/wxpython/modules/colorrules.py b/gui/wxpython/modules/colorrules.py index 0772dc7f9d5..b5b1047382e 100644 --- a/gui/wxpython/modules/colorrules.py +++ b/gui/wxpython/modules/colorrules.py @@ -41,7 +41,7 @@ from gui_core.gselect import Select, LayerSelect, ColumnSelect, VectorDBInfo from core.render import Map from gui_core.forms import GUI -from core.debug import Debug as Debug +from core.debug import Debug from gui_core.widgets import ColorTablesComboBox from gui_core.wrap import ( SpinCtrl, diff --git a/gui/wxpython/modules/import_export.py b/gui/wxpython/modules/import_export.py index c7b8897e783..15d4f76b586 100644 --- a/gui/wxpython/modules/import_export.py +++ b/gui/wxpython/modules/import_export.py @@ -274,7 +274,6 @@ def OnClose(self, event=None): def OnRun(self, event): """Import/Link data (each layes as separate vector map)""" - pass def OnCheckOverwrite(self, event): """Check/uncheck overwrite checkbox widget""" @@ -344,11 +343,9 @@ def OnAbort(self, event): .. todo:: not yet implemented """ - pass def OnCmdDone(self, event): """Do what has to be done after importing""" - pass def _getLayersToReprojetion(self, projMatch_idx, grassName_idx): """If there are layers with different projection from loation projection, diff --git a/gui/wxpython/nviz/mapwindow.py b/gui/wxpython/nviz/mapwindow.py index bbd6bcaa3bb..eaf0d266d4e 100644 --- a/gui/wxpython/nviz/mapwindow.py +++ b/gui/wxpython/nviz/mapwindow.py @@ -308,8 +308,8 @@ def ComputeMxMy(self, x, y): else: my = 0.0 - mx = mx / (1.0 - dx) - my = my / (1.0 - dy) + mx /= 1.0 - dx + my /= 1.0 - dy # Quadratic seems smoother mx *= abs(mx) @@ -2472,7 +2472,7 @@ def NvizCmdCommand(self): cmd += mode[3] if "wire" in mode[4]: cmd += mode[4] - if "coarse" in mode[0] or "both" in mode[0] and "wire" in mode[3]: + if "coarse" in mode[0] or ("both" in mode[0] and "wire" in mode[3]): cmd += mode[5] # # attributes @@ -2751,8 +2751,6 @@ def ZoomToMap(self, layers): def DisactivateWin(self): """Use when the class instance is hidden in MapFrame.""" - pass def ActivateWin(self): """Used when the class instance is activated in MapFrame.""" - pass diff --git a/gui/wxpython/nviz/tools.py b/gui/wxpython/nviz/tools.py index 02341420cc3..a1b0a9fcb1b 100644 --- a/gui/wxpython/nviz/tools.py +++ b/gui/wxpython/nviz/tools.py @@ -5860,8 +5860,8 @@ def Draw(self, pos, scale=False): w, h = self.GetClientSize() x, y = pos if scale: - x = x * w - y = y * h + x *= w + y *= h self.pdc.Clear() self.pdc.BeginDrawing() self.pdc.DrawLine(w // 2, h // 2, int(x), int(y)) diff --git a/gui/wxpython/photo2image/ip2i_manager.py b/gui/wxpython/photo2image/ip2i_manager.py index b4d24d19430..042053b65e2 100644 --- a/gui/wxpython/photo2image/ip2i_manager.py +++ b/gui/wxpython/photo2image/ip2i_manager.py @@ -596,7 +596,6 @@ def __del__(self): """Disable GCP manager mode""" # leaving the method here but was used only to delete gcpmanagement # from layer manager which is now not needed - pass def CreateGCPList(self): """Create GCP List Control""" @@ -1602,7 +1601,6 @@ def OnIdle(self, event): self.resize = False elif self.resize: event.RequestMore() - pass class GCPDisplay(FrameMixin, GCPPanel): diff --git a/gui/wxpython/psmap/dialogs.py b/gui/wxpython/psmap/dialogs.py index 740678de3cb..216782037be 100644 --- a/gui/wxpython/psmap/dialogs.py +++ b/gui/wxpython/psmap/dialogs.py @@ -2103,7 +2103,6 @@ def OnApply(self, event): def updateDialog(self): """Update information (not used)""" - pass # if "map" in self.parent.openDialogs: @@ -2149,7 +2148,6 @@ def OnApply(self, event): def updateDialog(self): """Update information (not used)""" - pass class VPropertiesDialog(Dialog): @@ -6729,7 +6727,6 @@ def update(self): def updateDialog(self): """Update text coordinates, after moving""" - pass class LabelsDialog(PsmapDialog): diff --git a/gui/wxpython/psmap/frame.py b/gui/wxpython/psmap/frame.py index 8725a0a957d..3effc33cd4d 100644 --- a/gui/wxpython/psmap/frame.py +++ b/gui/wxpython/psmap/frame.py @@ -569,7 +569,7 @@ def getFile(self, wildcard): filename = dlg.GetPath() suffix = suffix[dlg.GetFilterIndex()] if not os.path.splitext(filename)[1]: - filename = filename + suffix + filename += suffix elif suffix not in {os.path.splitext(filename)[1], ""}: filename = os.path.splitext(filename)[0] + suffix @@ -1509,8 +1509,8 @@ def SetPage(self): if self.currScale is None: self.currScale = min(cW / pW, cH / pH) - pW = pW * self.currScale - pH = pH * self.currScale + pW *= self.currScale + pH *= self.currScale x = cW / 2 - pW / 2 y = cH / 2 - pH / 2 @@ -2230,9 +2230,7 @@ def ComputeZoom(self, rect): zoomFactor = 1 # when zooming to full extent, in some cases, there was zoom # 1.01..., which causes problem - if abs(zoomFactor - 1) > 0.01: - zoomFactor = zoomFactor - else: + if abs(zoomFactor - 1) <= 0.01: zoomFactor = 1.0 if self.mouse["use"] == "zoomout": @@ -2259,10 +2257,10 @@ def Zoom(self, zoomFactor, view): """Zoom to specified region, scroll view, redraw""" if not self.currScale: return - self.currScale = self.currScale * zoomFactor + self.currScale *= zoomFactor if self.currScale > 10 or self.currScale < 0.1: - self.currScale = self.currScale / zoomFactor + self.currScale /= zoomFactor return if not self.preview: # redraw paper @@ -2608,8 +2606,8 @@ def ImageRect(self): iW, iH = img.GetWidth(), img.GetHeight() self.currScale = min(float(cW) / iW, float(cH) / iH) - iW = iW * self.currScale - iH = iH * self.currScale + iW *= self.currScale + iH *= self.currScale x = cW / 2 - iW / 2 y = cH / 2 - iH / 2 return Rect(int(x), int(y), int(iW), int(iH)) @@ -2716,11 +2714,8 @@ def UpdateLabel(self, itype, id): def OnSize(self, event): """Init image size to match window size""" # not zoom all when notebook page is changed - if ( - self.preview - and self.parent.currentPage == 1 - or not self.preview - and self.parent.currentPage == 0 + if (self.preview and self.parent.currentPage == 1) or ( + not self.preview and self.parent.currentPage == 0 ): self.ZoomAll() self.OnIdle(None) diff --git a/gui/wxpython/psmap/instructions.py b/gui/wxpython/psmap/instructions.py index f52bad96069..2ae68a91069 100644 --- a/gui/wxpython/psmap/instructions.py +++ b/gui/wxpython/psmap/instructions.py @@ -474,11 +474,8 @@ def SendToRead(self, instruction, text, **kwargs): for line in text: if line.find("# north arrow") >= 0: commentFound = True - if ( - i == "image" - and commentFound - or i == "northArrow" - and not commentFound + if (i == "image" and commentFound) or ( + i == "northArrow" and not commentFound ): continue newInstr = myInstrDict[i](id, settings=self, env=self.env) @@ -573,7 +570,6 @@ def SetInstruction(self, instruction): def Read(self, instruction, text, **kwargs): """Read instruction and save them""" - pass def PercentToReal(self, e, n): """Converts text coordinates from percent of region to map coordinates""" @@ -1737,17 +1733,13 @@ def Read(self, instruction, text, **kwargs): def EstimateHeight(self, raster, discrete, fontsize, cols=None, height=None): """Estimate height to draw raster legend""" if discrete == "n": - if height: - height = height - else: + if not height: height = self.unitConv.convert( value=fontsize * 10, fromUnit="point", toUnit="inch" ) if discrete == "y": - if cols: - cols = cols - else: + if not cols: cols = 1 rinfo = gs.raster_info(raster) @@ -1776,9 +1768,7 @@ def EstimateWidth( if discrete == "n": rinfo = gs.raster_info(raster) minim, maxim = rinfo["min"], rinfo["max"] - if width: - width = width - else: + if not width: width = self.unitConv.convert( value=fontsize * 2, fromUnit="point", toUnit="inch" ) @@ -1789,14 +1779,10 @@ def EstimateWidth( width += textPart elif discrete == "y": - if cols: - cols = cols - else: + if not cols: cols = 1 - if width: - width = width - else: + if not width: paperWidth = ( paperInstr["Width"] - paperInstr["Right"] - paperInstr["Left"] ) @@ -1876,14 +1862,10 @@ def Read(self, instruction, text, **kwargs): def EstimateSize(self, vectorInstr, fontsize, width=None, cols=None): """Estimate size to draw vector legend""" - if width: - width = width - else: + if not width: width = fontsize / 24.0 - if cols: - cols = cols - else: + if not cols: cols = 1 vectors = vectorInstr["list"] diff --git a/gui/wxpython/rdigit/g.gui.rdigit.py b/gui/wxpython/rdigit/g.gui.rdigit.py index a05fdb547bb..81f15d15531 100755 --- a/gui/wxpython/rdigit/g.gui.rdigit.py +++ b/gui/wxpython/rdigit/g.gui.rdigit.py @@ -139,7 +139,7 @@ def __init__( rdigit.OnMapSelection() # use Close instead of QuitRDigit for standalone tool self.rdigit.quitDigitizer.disconnect(self.QuitRDigit) - self.rdigit.quitDigitizer.connect(self.Close) + self.rdigit.quitDigitizer.connect(lambda: self.Close()) # add Map Display panel to Map Display frame sizer = wx.BoxSizer(wx.VERTICAL) diff --git a/gui/wxpython/rlisetup/sampling_frame.py b/gui/wxpython/rlisetup/sampling_frame.py index dcd8398d049..860c0b91d8f 100644 --- a/gui/wxpython/rlisetup/sampling_frame.py +++ b/gui/wxpython/rlisetup/sampling_frame.py @@ -264,7 +264,7 @@ def writeArea(self, coords, rasterName): catbuf = "=%d a\n" % self.catId polyfile.write(catbuf) - self.catId = self.catId + 1 + self.catId += 1 polyfile.close() region_settings = grass.parse_command("g.region", flags="p", delimiter=":") diff --git a/gui/wxpython/rlisetup/wizard.py b/gui/wxpython/rlisetup/wizard.py index 12888948ec3..54c6dbcd78a 100644 --- a/gui/wxpython/rlisetup/wizard.py +++ b/gui/wxpython/rlisetup/wizard.py @@ -1217,7 +1217,7 @@ def afterRegionDrawn(self, marea): if marea: self.parent.msAreaList.append(marea) - self.regioncount = self.regioncount + 1 + self.regioncount += 1 numregions = int(self.parent.samplingareapage.numregions) if self.regioncount > numregions: @@ -1715,7 +1715,7 @@ def SampleFrameChanged(self, region): # region = self.GetSampleUnitRegion() if region: self.parent.msAreaList.append(region) - self.regioncount = self.regioncount + 1 + self.regioncount += 1 drawtype = self.parent.drawunits.drawtype if self.regioncount > self.numregions: @@ -1780,7 +1780,6 @@ def OnEnterPage(self, event): def OnExitPage(self, event=None): """Function during exiting""" - pass # if event.GetDirection(): # self.SetNext(self.parent.samplingareapage) @@ -1814,7 +1813,7 @@ def __init__(self, wizard, parent): def afterRegionDrawn(self): """Function to update the title and the number of selected area""" - self.areascount = self.areascount + 1 + self.areascount += 1 if self.areascount == self.areanum: wx.FindWindowById(wx.ID_FORWARD).Enable(True) self.areaOK.Enable(False) diff --git a/gui/wxpython/timeline/frame.py b/gui/wxpython/timeline/frame.py index bba9b7a3717..f69dcc89aef 100644 --- a/gui/wxpython/timeline/frame.py +++ b/gui/wxpython/timeline/frame.py @@ -589,11 +589,11 @@ def AddDataset(self, type_, yrange, xranges, datasetName): if type_ == "bar": self.data[yrange] = {"name": datasetName} for i, (start, end) in enumerate(xranges): - self.data[yrange][(start, end)] = i + self.data[yrange][start, end] = i elif type_ == "point": - self.data[(yrange, yrange)] = {"name": datasetName} + self.data[yrange, yrange] = {"name": datasetName} for i, start in enumerate(xranges): - self.data[(yrange, yrange)][(start, start)] = i + self.data[yrange, yrange][start, start] = i def GetInformation(self, x, y): keys = None diff --git a/gui/wxpython/vdigit/dialogs.py b/gui/wxpython/vdigit/dialogs.py index 395f92a4102..43b277b2ca7 100644 --- a/gui/wxpython/vdigit/dialogs.py +++ b/gui/wxpython/vdigit/dialogs.py @@ -591,7 +591,7 @@ def Populate(self, cats, update=False): self.SetItem(index, 1, str(cat)) self.SetItemData(index, i) itemData[i] = (str(layer), str(cat)) - i = i + 1 + i += 1 if not update: self.SetColumnWidth(0, 100) @@ -774,4 +774,3 @@ def LoadData(self, data): def OnCheckItem(self, index, flag): """Mapset checked/unchecked""" - pass diff --git a/gui/wxpython/vdigit/g.gui.vdigit.py b/gui/wxpython/vdigit/g.gui.vdigit.py index ea09f207909..be476a03ee1 100644 --- a/gui/wxpython/vdigit/g.gui.vdigit.py +++ b/gui/wxpython/vdigit/g.gui.vdigit.py @@ -104,7 +104,7 @@ def __init__(self, parent, vectorMap): self.toolbars["vdigit"].StartEditing(mapLayer) # use Close instead of QuitVDigit for standalone tool self.toolbars["vdigit"].quitDigitizer.disconnect(self.QuitVDigit) - self.toolbars["vdigit"].quitDigitizer.connect(self.Close) + self.toolbars["vdigit"].quitDigitizer.connect(lambda: self.Close()) # add Map Display panel to Map Display frame sizer = wx.BoxSizer(wx.VERTICAL) diff --git a/gui/wxpython/vdigit/preferences.py b/gui/wxpython/vdigit/preferences.py index b825748a025..e565c60dfe9 100644 --- a/gui/wxpython/vdigit/preferences.py +++ b/gui/wxpython/vdigit/preferences.py @@ -797,8 +797,6 @@ def OnChangeLayer(self, event): def OnChangeAddRecord(self, event): """Checkbox 'Add new record' status changed""" - pass - # self.category.SetValue(self.digit.SetCategory()) def OnChangeSnappingValue(self, event): """Change snapping value - update static text""" diff --git a/gui/wxpython/vdigit/toolbars.py b/gui/wxpython/vdigit/toolbars.py index 1e8d394bddc..6c5c8b21212 100644 --- a/gui/wxpython/vdigit/toolbars.py +++ b/gui/wxpython/vdigit/toolbars.py @@ -565,9 +565,9 @@ def OnAddAreaMenu(self, event): menuItems = [] if not self.tools or "addArea" in self.tools: menuItems.append((self.icons["addArea"], self.OnAddArea)) - if not self.fType and not self.tools or "addBoundary" in self.tools: + if (not self.fType and not self.tools) or "addBoundary" in self.tools: menuItems.append((self.icons["addBoundary"], self.OnAddBoundary)) - if not self.fType and not self.tools or "addCentroid" in self.tools: + if (not self.fType and not self.tools) or "addCentroid" in self.tools: menuItems.append((self.icons["addCentroid"], self.OnAddCentroid)) self._onMenu(menuItems) diff --git a/gui/wxpython/web_services/widgets.py b/gui/wxpython/web_services/widgets.py index 67d000d75df..83fe7a5706c 100644 --- a/gui/wxpython/web_services/widgets.py +++ b/gui/wxpython/web_services/widgets.py @@ -171,9 +171,9 @@ def _requestPage(self): style = wx.TR_DEFAULT_STYLE | wx.TR_HAS_BUTTONS | wx.TR_FULL_ROW_HIGHLIGHT if self.drv_props["req_multiple_layers"]: - style = style | wx.TR_MULTIPLE + style |= wx.TR_MULTIPLE if "WMS" not in self.ws: - style = style | wx.TR_HIDE_ROOT + style |= wx.TR_HIDE_ROOT self.list = LayersList( parent=self.req_page_panel, web_service=self.ws, style=style @@ -1096,11 +1096,8 @@ def compare(item, l_name, st_name) -> bool: return bool( it_l_name == l_name and ( - not it_st - and not st_name - or it_st - and it_st["name"] == st_name - and it_type == "style" + (not it_st and not st_name) + or (it_st and it_st["name"] == st_name and it_type == "style") ) ) diff --git a/imagery/i.aster.toar/main.c b/imagery/i.aster.toar/main.c index 5dc94a51d1e..8536966882b 100644 --- a/imagery/i.aster.toar/main.c +++ b/imagery/i.aster.toar/main.c @@ -165,21 +165,21 @@ int main(int argc, char *argv[]) /*Prepare the output file names */ /********************/ - sprintf(result0, "%s%s", result, ".1"); - sprintf(result1, "%s%s", result, ".2"); - sprintf(result2, "%s%s", result, ".3N"); - sprintf(result3, "%s%s", result, ".3B"); - sprintf(result4, "%s%s", result, ".4"); - sprintf(result5, "%s%s", result, ".5"); - sprintf(result6, "%s%s", result, ".6"); - sprintf(result7, "%s%s", result, ".7"); - sprintf(result8, "%s%s", result, ".8"); - sprintf(result9, "%s%s", result, ".9"); - sprintf(result10, "%s%s", result, ".10"); - sprintf(result11, "%s%s", result, ".11"); - sprintf(result12, "%s%s", result, ".12"); - sprintf(result13, "%s%s", result, ".13"); - sprintf(result14, "%s%s", result, ".14"); + snprintf(result0, sizeof(result0), "%s%s", result, ".1"); + snprintf(result1, sizeof(result1), "%s%s", result, ".2"); + snprintf(result2, sizeof(result2), "%s%s", result, ".3N"); + snprintf(result3, sizeof(result3), "%s%s", result, ".3B"); + snprintf(result4, sizeof(result4), "%s%s", result, ".4"); + snprintf(result5, sizeof(result5), "%s%s", result, ".5"); + snprintf(result6, sizeof(result6), "%s%s", result, ".6"); + snprintf(result7, sizeof(result7), "%s%s", result, ".7"); + snprintf(result8, sizeof(result8), "%s%s", result, ".8"); + snprintf(result9, sizeof(result9), "%s%s", result, ".9"); + snprintf(result10, sizeof(result10), "%s%s", result, ".10"); + snprintf(result11, sizeof(result11), "%s%s", result, ".11"); + snprintf(result12, sizeof(result12), "%s%s", result, ".12"); + snprintf(result13, sizeof(result13), "%s%s", result, ".13"); + snprintf(result14, sizeof(result14), "%s%s", result, ".14"); /********************/ /*Prepare radiance boundaries */ diff --git a/imagery/i.atcorr/create_iwave.py b/imagery/i.atcorr/create_iwave.py index 243e7a22542..022fdf8d12e 100644 --- a/imagery/i.atcorr/create_iwave.py +++ b/imagery/i.atcorr/create_iwave.py @@ -217,12 +217,12 @@ def write_cpp(bands, values, sensor, folder): # Get minimum wavelength with spectral response c = maxresponse_idx while c > 0 and fi[c - 1] > rthresh: - c = c - 1 + c -= 1 min_wavelength = np.ceil(li[0] * 1000 + (2.5 * c)) # Get maximum wavelength with spectral response c = maxresponse_idx while c < len(fi) - 1 and fi[c + 1] > rthresh: - c = c + 1 + c += 1 max_wavelength = np.floor(li[0] * 1000 + (2.5 * c)) print(" %s (%inm - %inm)" % (bands[b], min_wavelength, max_wavelength)) @@ -239,12 +239,12 @@ def write_cpp(bands, values, sensor, folder): # Get minimum wavelength with spectral response c = maxresponse_idx while c > 0 and fi[c - 1] > rthresh: - c = c - 1 + c -= 1 min_wavelength = np.ceil(li[0] * 1000 + (2.5 * c)) # Get maximum wavelength with spectral response c = maxresponse_idx while c < len(fi) - 1 and fi[c + 1] > rthresh: - c = c + 1 + c += 1 max_wavelength = np.floor(li[0] * 1000 + (2.5 * c)) print(" %s (%inm - %inm)" % (bands[b], min_wavelength, max_wavelength)) diff --git a/imagery/i.ortho.photo/i.ortho.photo/menu.c b/imagery/i.ortho.photo/i.ortho.photo/menu.c index 76bfe6a3f04..40313c6c891 100644 --- a/imagery/i.ortho.photo/i.ortho.photo/menu.c +++ b/imagery/i.ortho.photo/i.ortho.photo/menu.c @@ -24,6 +24,8 @@ #include #include "orthophoto.h" +#define BUF_SIZE 99 + int main(int argc, char **argv) { char *p; @@ -33,7 +35,8 @@ int main(int argc, char **argv) char *desc_ortho_opt; char *moduletorun; const char *grname; - char tosystem[99]; + char tosystem[BUF_SIZE] = ""; + size_t len; /* initialize grass */ G_gisinit(argv[0]); @@ -82,8 +85,10 @@ int main(int argc, char **argv) /* group validity check */ /*----------------------*/ - strncpy(group.name, group_opt->answer, 99); - group.name[99] = '\0'; + len = G_strlcpy(group.name, group_opt->answer, BUF_SIZE); + if (len >= BUF_SIZE) { + G_fatal_error(_("Name <%s> is too long"), group_opt->answer); + } /* strip off mapset if it's there: I_() fns only work with current mapset */ if ((p = strchr(group.name, '@'))) *p = 0; @@ -96,26 +101,26 @@ int main(int argc, char **argv) moduletorun = ortho_opt->answer; /* run the program chosen */ if (strcmp(moduletorun, "g.gui.photo2image") == 0) { - strcpy(tosystem, "g.gui.photo2image"); + (void)G_strlcpy(tosystem, "g.gui.photo2image", BUF_SIZE); return system((const char *)tosystem); } else if (strcmp(moduletorun, "g.gui.image2target") == 0) { - strcpy(tosystem, "g.gui.image2target"); + (void)G_strlcpy(tosystem, "g.gui.image2target", BUF_SIZE); return system((const char *)tosystem); } else { if (strcmp(moduletorun, "i.group") == 0) - strcpy(tosystem, "i.group --ui group="); + (void)G_strlcpy(tosystem, "i.group --ui group=", BUF_SIZE); if (strcmp(moduletorun, "i.ortho.target") == 0) - strcpy(tosystem, "i.ortho.target --ui group="); + (void)G_strlcpy(tosystem, "i.ortho.target --ui group=", BUF_SIZE); if (strcmp(moduletorun, "i.ortho.elev") == 0) - strcpy(tosystem, "i.ortho.elev --ui group="); + (void)G_strlcpy(tosystem, "i.ortho.elev --ui group=", BUF_SIZE); if (strcmp(moduletorun, "i.ortho.camera") == 0) - strcpy(tosystem, "i.ortho.camera --ui group="); + (void)G_strlcpy(tosystem, "i.ortho.camera --ui group=", BUF_SIZE); if (strcmp(moduletorun, "i.ortho.init") == 0) - strcpy(tosystem, "i.ortho.init --ui group="); + (void)G_strlcpy(tosystem, "i.ortho.init --ui group=", BUF_SIZE); if (strcmp(moduletorun, "i.ortho.rectify") == 0) - strcpy(tosystem, "i.ortho.rectify --ui group="); + (void)G_strlcpy(tosystem, "i.ortho.rectify --ui group=", BUF_SIZE); strcat(tosystem, grname); return system((const char *)tosystem); } diff --git a/imagery/i.smap/multialloc.c b/imagery/i.smap/multialloc.c index a4472f74db5..e3be7693990 100644 --- a/imagery/i.smap/multialloc.c +++ b/imagery/i.smap/multialloc.c @@ -37,7 +37,7 @@ char *multialloc(size_t s, /* individual array element size */ for (i = 0; i < d - 1; i++, q++) { /* for each of the dimensions * but the last */ max *= (*q); - r[0] = (char *)G_malloc(max * sizeof(char **)); + r[0] = (char *)G_malloc(max * sizeof(char *)); r = (char **)r[0]; /* step through to beginning of next * dimension array */ } diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index 1eacd9ed933..4125a3aeed3 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -548,6 +548,9 @@ int G_name_is_fully_qualified(const char *, char *, char *); char *G_fully_qualified_name(const char *, const char *); int G_unqualified_name(const char *, const char *, char *, char *); +/* omp_threads.c */ +int G_set_omp_num_threads(struct Option *); + /* open.c */ int G_open_new(const char *, const char *); int G_open_old(const char *, const char *, const char *); diff --git a/include/grass/defs/vector.h b/include/grass/defs/vector.h index a5e1618a8cb..42ed74c532b 100644 --- a/include/grass/defs/vector.h +++ b/include/grass/defs/vector.h @@ -616,9 +616,11 @@ GEOSGeometry *Vect_line_to_geos(const struct line_pnts *, int, int); GEOSGeometry *Vect_read_area_geos(struct Map_info *, int); GEOSCoordSequence *Vect_get_area_points_geos(struct Map_info *, int); GEOSCoordSequence *Vect_get_isle_points_geos(struct Map_info *, int); -char *Vect_line_to_wkt(const struct line_pnts *, int, int); +char *Vect_line_to_wkt(const struct line_pnts *, int, bool); +char *Vect_line_to_wkt2(const struct line_pnts *, int, bool, bool); unsigned char *Vect_line_to_wkb(const struct line_pnts *, int, int, size_t *); char *Vect_read_area_to_wkt(struct Map_info *, int); +char *Vect_read_area_to_wkt2(struct Map_info *, int, bool); unsigned char *Vect_read_area_to_wkb(struct Map_info *, int, size_t *); unsigned char *Vect_read_line_to_wkb(struct Map_info *, struct line_pnts *, struct line_cats *, int, size_t *, int *); diff --git a/lib/gis/Makefile b/lib/gis/Makefile index a1579dbf862..90b39f374a4 100644 --- a/lib/gis/Makefile +++ b/lib/gis/Makefile @@ -2,8 +2,9 @@ MODULE_TOPDIR = ../.. LIB = GIS -EXTRA_INC = $(ZLIBINCPATH) $(BZIP2INCPATH) $(ZSTDINCPATH) $(PTHREADINCPATH) $(REGEXINCPATH) -EXTRA_CFLAGS = -DGRASS_VERSION_DATE=\"'$(GRASS_VERSION_DATE)'\" +LIBES = $(OPENMP_LIBPATH) $(OPENMP_LIB) +EXTRA_INC = $(ZLIBINCPATH) $(BZIP2INCPATH) $(ZSTDINCPATH) $(PTHREADINCPATH) $(REGEXINCPATH) $(OPENMP_INCPATH) +EXTRA_CFLAGS = $(OPENMP_CFLAGS) -DGRASS_VERSION_DATE=\"'$(GRASS_VERSION_DATE)'\" PROJSRC = ellipse.table ellipse.table.solar.system datum.table \ datumtransform.table FIPS.code state27 state83 projections diff --git a/lib/gis/home.c b/lib/gis/home.c index 166f6f2f191..e39cbff49ca 100644 --- a/lib/gis/home.c +++ b/lib/gis/home.c @@ -100,15 +100,20 @@ const char *G_config_path(void) static int initialized_config; static const char *config_path = 0; char buf[GPATH_MAX]; + static const char *config_dir = NULL; if (G_is_initialized(&initialized_config)) return config_path; + config_dir = getenv("GRASS_CONFIG_DIR"); + if (!config_dir) #ifdef __MINGW32__ - sprintf(buf, "%s%c%s", getenv("APPDATA"), HOST_DIRSEP, CONFIG_DIR); + config_dir = getenv("APPDATA"); #else - sprintf(buf, "%s%c%s", G_home(), HOST_DIRSEP, CONFIG_DIR); + config_dir = G_home(); #endif + + snprintf(buf, GPATH_MAX, "%s%c%s", config_dir, HOST_DIRSEP, CONFIG_DIR); config_path = G_store(buf); #if 0 diff --git a/lib/gis/omp_threads.c b/lib/gis/omp_threads.c new file mode 100644 index 00000000000..f5169d8624e --- /dev/null +++ b/lib/gis/omp_threads.c @@ -0,0 +1,45 @@ +#if defined(_OPENMP) +#include +#endif + +#include +#include +#include +#include +#include + +/*! \brief Set the number of threads for OpenMP + The intended usage is at the beginning of a C tool when parameters are + processed, namely the G_OPT_M_NPROCS standard option. + + \param opt A nprocs Option struct to specify the number of threads + \return the number of threads set up for OpenMP parallel computing +*/ + +int G_set_omp_num_threads(struct Option *opt) +{ + /* make sure Option is not null */ + if (opt == NULL) + G_fatal_error(_("Option is NULL.")); + else if (opt->key == NULL) + G_fatal_error(_("Option key is NULL.")); + + int threads = atoi(opt->answer); +#if defined(_OPENMP) + int num_logic_procs = omp_get_num_procs(); + if (threads < 1) { + threads += num_logic_procs; + threads = (threads < 1) ? 1 : threads; + } + omp_set_num_threads(threads); + G_verbose_message(_("%d threads are set up for parallel computing."), + threads); +#else + if (threads != 1) { + G_warning(_("GRASS GIS is not compiled with OpenMP support, parallel " + "computation is disabled. Only one thread will be used.")); + threads = 1; + } +#endif + return threads; +} diff --git a/lib/init/grass.py b/lib/init/grass.py index 1231e299049..2a9a585b92b 100755 --- a/lib/init/grass.py +++ b/lib/init/grass.py @@ -306,6 +306,7 @@ def f(fmt, *args): FLAG {standard_flags} {env_vars}: + GRASS_CONFIG_DIR {config_dir_var} GRASS_GUI {gui_var} GRASS_HTML_BROWSER {html_var} GRASS_ADDON_PATH {addon_path_var} @@ -349,6 +350,7 @@ def help_message(default_gui): mapset=_("initial GRASS mapset"), full_mapset=_("fully qualified initial mapset directory"), env_vars=_("Environment variables relevant for startup"), + config_dir_var=_("set root path for configuration directory"), gui_var=_("select GUI (text, gui, gtext)"), html_var=_("set html web browser for help pages"), addon_path_var=_( @@ -379,8 +381,8 @@ def help_message(default_gui): sys.stderr.write(s) -def get_grass_config_dir(): - """Get configuration directory +def create_grass_config_dir(): + """Create configuration directory Determines path of GRASS GIS user configuration directory and creates it if it does not exist. @@ -388,41 +390,25 @@ def get_grass_config_dir(): Configuration directory is for example used for grass env file (the one which caries mapset settings from session to session). """ - # The code is in sync with grass.app.runtime (but not the same). - if WINDOWS: - grass_config_dirname = f"GRASS{GRASS_VERSION_MAJOR}" - win_conf_path = os.getenv("APPDATA") - # this can happen with some strange settings - if not win_conf_path: - fatal( - _( - "The APPDATA variable is not set, ask your operating" - " system support" - ) - ) - if not os.path.exists(win_conf_path): - fatal( - _( - "The APPDATA variable points to directory which does" - " not exist, ask your operating system support" - ) - ) - directory = os.path.join(win_conf_path, grass_config_dirname) - elif MACOS: - version = f"{GRASS_VERSION_MAJOR}.{GRASS_VERSION_MINOR}" - return os.path.join(os.getenv("HOME"), "Library", "GRASS", version) - else: - grass_config_dirname = f".grass{GRASS_VERSION_MAJOR}" - directory = os.path.join(os.getenv("HOME"), grass_config_dirname) + from grass.app.runtime import get_grass_config_dir + + try: + directory = get_grass_config_dir( + GRASS_VERSION_MAJOR, GRASS_VERSION_MINOR, os.environ + ) + except (RuntimeError, NotADirectoryError) as e: + fatal(f"{e}") + if not os.path.isdir(directory): try: - os.mkdir(directory) + os.makedirs(directory) except OSError as e: # Can happen as a race condition if not e.errno == errno.EEXIST or not os.path.isdir(directory): fatal( - _("Failed to create configuration directory '%s' with error: %s") - % (directory, e.strerror) + _( + "Failed to create configuration directory '{}' with error: {}" + ).format(directory, e.strerror) ) return directory @@ -1251,10 +1237,8 @@ def set_language(grass_config_dir): # If we got so far, attempts to set up language and locale have # failed on this system. sys.stderr.write( - ( - "Failed to enforce user specified language " - f"'{language}' with error: '{e}'\n" - ) + "Failed to enforce user specified language " + f"'{language}' with error: '{e}'\n" ) sys.stderr.write( "A LANGUAGE environmental variable has been set.\n" @@ -2233,7 +2217,8 @@ def main(): # This has to be called before any _() function call! # Subsequent functions are using _() calls and # thus must be called only after Language has been set. - grass_config_dir = get_grass_config_dir() + find_grass_python_package() + grass_config_dir = create_grass_config_dir() set_language(grass_config_dir) # Set default GUI @@ -2296,8 +2281,6 @@ def main(): # Create the session grassrc file gisrc = create_gisrc(tmpdir, gisrcrc) - find_grass_python_package() - from grass.app.runtime import ( ensure_home, set_paths, diff --git a/lib/init/variables.html b/lib/init/variables.html index 4c84a7803ac..6fd10d7f173 100644 --- a/lib/init/variables.html +++ b/lib/init/variables.html @@ -124,6 +124,13 @@

List of selected (GRASS related) shell environment variables

available are RLE, ZLIB, and LZ4. The compressors BZIP2 and ZSTD must be enabled when configuring GRASS for compilation. +
GRASS_CONFIG_DIR
+
[grass startup script]
+ specifies root path for GRASS configuration directory. + If not specified, the default placement of the + configuration directory is used: $HOME on GNU/Linux, + $HOME/Library on Mac OS X, and %APPDATA% on MS Windows.
+
GRASS_DB_ENCODING
[various modules, wxGUI]
encoding for vector attribute data (utf-8, ascii, iso8859-1, koi8-r)
diff --git a/lib/vector/Vlib/geos_to_wktb.c b/lib/vector/Vlib/geos_to_wktb.c index 5d549eb4a11..ce84dde7f0c 100644 --- a/lib/vector/Vlib/geos_to_wktb.c +++ b/lib/vector/Vlib/geos_to_wktb.c @@ -13,6 +13,7 @@ \author Soeren Gebbert */ +#include #include #include #include @@ -62,16 +63,30 @@ unsigned char *Vect_read_area_to_wkb(struct Map_info *Map, int area, /*! \brief Read vector area and return it as Well Known Text (WKT) - unsigned char array + unsigned char array + + Calls Vect_read_area_to_wkt2() with trim set to false. + + */ +char *Vect_read_area_to_wkt(struct Map_info *Map, int area) +{ + return Vect_read_area_to_wkt2(Map, area, false); +} + +/*! + \brief Read vector area and return it as Well Known Text (WKT) + unsigned char array \param Map pointer to Map_info structure \param area area id \param size The size of the returned unsigned char array + \param trim Set the number trimming option on, With trim set to true, the + writer will strip trailing 0's from the output coordinates. - \return pointer to char array + \return pointer to string (allocated) \return NULL on error */ -char *Vect_read_area_to_wkt(struct Map_info *Map, int area) +char *Vect_read_area_to_wkt2(struct Map_info *Map, int area, bool trim) { static int init = 0; @@ -86,18 +101,21 @@ char *Vect_read_area_to_wkt(struct Map_info *Map, int area) } GEOSWKTWriter_setOutputDimension(writer, 2); + GEOSWKTWriter_setTrim(writer, trim); GEOSGeometry *geom = Vect_read_area_geos(Map, area); if (!geom) { - return (NULL); + return NULL; } wkt = GEOSWKTWriter_write(writer, geom); + char *wkt_out = G_store(wkt); GEOSGeom_destroy(geom); + GEOSFree(wkt); - return (wkt); + return wkt_out; } /*! @@ -199,7 +217,7 @@ unsigned char *Vect_read_line_to_wkb(struct Map_info *Map, \param with_z Set to 1 if the feature is 3d, 0 otherwise \param size The size of the returned byte array - \return pointer to char array + \return pointer to string (allocated) \return NULL on error */ unsigned char *Vect_line_to_wkb(const struct line_pnts *points, int type, @@ -234,7 +252,18 @@ unsigned char *Vect_line_to_wkb(const struct line_pnts *points, int type, /*! \brief Create a Well Known Text (WKT) representation of - given feature type from points. + given feature type from points. + + Calls Vect_line_to_wkt2() with trim set to false. + */ +char *Vect_line_to_wkt(const struct line_pnts *points, int type, bool with_z) +{ + return Vect_line_to_wkt2(points, type, with_z, false); +} + +/*! + \brief Create a Well Known Text (WKT) representation of + given feature type from points. This function is not thread safe, it uses static variables for speedup. @@ -246,12 +275,15 @@ unsigned char *Vect_line_to_wkb(const struct line_pnts *points, int type, \param points pointer to line_pnts structure \param type feature type (see supported types) - \param with_z Set to 1 if the feature is 3d, 0 otherwise + \param with_z Set to true if the feature is 3d, false otherwise + \param trim Set the number trimming option on, With trim set to true, the + writer will strip trailing 0's from the output coordinates. \return pointer to char array \return NULL on error */ -char *Vect_line_to_wkt(const struct line_pnts *points, int type, int with_z) +char *Vect_line_to_wkt2(const struct line_pnts *points, int type, bool with_z, + bool trim) { static int init = 0; @@ -266,18 +298,21 @@ char *Vect_line_to_wkt(const struct line_pnts *points, int type, int with_z) } GEOSWKTWriter_setOutputDimension(writer, with_z ? 3 : 2); + GEOSWKTWriter_setTrim(writer, trim); GEOSGeometry *geom = Vect_line_to_geos(points, type, with_z); if (!geom) { - return (NULL); + return NULL; } wkt = GEOSWKTWriter_write(writer, geom); + char *wkt_out = G_store(wkt); GEOSGeom_destroy(geom); + GEOSFree(wkt); - return (wkt); + return wkt_out; } #endif /* HAVE_GEOS */ diff --git a/lib/vector/diglib/frmt.c b/lib/vector/diglib/frmt.c index 8e3612a8dfd..66d107c6a5e 100644 --- a/lib/vector/diglib/frmt.c +++ b/lib/vector/diglib/frmt.c @@ -19,6 +19,7 @@ #include #include +#include /*! \brief Read external vector format file @@ -34,6 +35,7 @@ int dig_read_frmt_ascii(FILE *dascii, struct Format_info *finfo) char buff[2001], buf1[2001]; char *ptr; int frmt = -1; + size_t len; G_debug(3, "dig_read_frmt_ascii()"); @@ -46,7 +48,11 @@ int dig_read_frmt_ascii(FILE *dascii, struct Format_info *finfo) return -1; } - strcpy(buf1, buff); + len = G_strlcpy(buf1, buff, sizeof(buf1)); + if (len >= sizeof(buf1)) { + G_warning(_("Line <%s> is too long"), buff); + return -1; + } buf1[ptr - buff] = '\0'; ptr++; /* Search for the start of text */ @@ -98,7 +104,11 @@ int dig_read_frmt_ascii(FILE *dascii, struct Format_info *finfo) continue; } - strcpy(buf1, buff); + len = G_strlcpy(buf1, buff, sizeof(buf1)); + if (len >= sizeof(buf1)) { + G_warning(_("Line <%s> is too long"), buff); + return -1; + } buf1[ptr - buff] = '\0'; ptr++; /* Search for the start of text */ diff --git a/man/build_html.py b/man/build_html.py index e075ebaafc1..daa38b557f4 100644 --- a/man/build_html.py +++ b/man/build_html.py @@ -440,7 +440,7 @@ def html_files(cls=None, ignore_gui=True): and (cls != "*" or len(cmd.split(".")) >= 3) and cmd not in {"full_index.html", "index.html"} and cmd not in exclude_mods - and (ignore_gui and not cmd.startswith("wxGUI.") or not ignore_gui) + and ((ignore_gui and not cmd.startswith("wxGUI.")) or not ignore_gui) ): yield cmd diff --git a/pyproject.toml b/pyproject.toml index 574b99df4cb..b9f4b73bccd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,12 +158,9 @@ ignore = [ "PERF401", # manual-list-comprehension "PERF402", # manual-list-copy "PERF403", # manual-dict-comprehension - "PIE790", # unnecessary-placeholder "PIE794", # duplicate-class-field-definition "PIE808", # unnecessary-range-start "PIE810", # multiple-starts-ends-with - "PLC0206", # dict-index-missing-items - "PLC0414", # useless-import-alias "PLC0415", # import-outside-top-level "PLC1901", # compare-to-empty-string "PLC2701", # import-private-name @@ -183,10 +180,8 @@ ignore = [ "PLR1704", # redefined-argument-from-local "PLR1733", # unnecessary-dict-index-lookup "PLR2004", # magic-value-comparison - "PLR6104", # non-augmented-assignment "PLR6201", # literal-membership "PLR6301", # no-self-use - "PLW0127", # self-assigning-variable "PLW0406", # import-self "PLW0602", # global-variable-not-assigned "PLW0603", # global-statement @@ -227,13 +222,13 @@ ignore = [ "PTH202", # os-path-getsize "PTH204", # os-path-getmtime "PTH207", # glob - "RET501", # unnecessary-return-none - "RET502", # implicit-return-value - "RET503", # implicit-return - "RET505", # superfluous-else-return - "RET506", # superfluous-else-raise - "RET507", # superfluous-else-continue - "RET508", # superfluous-else-break + "RET501", # unnecessary-return-none + "RET502", # implicit-return-value + "RET503", # implicit-return + "RET505", # superfluous-else-return + "RET506", # superfluous-else-raise + "RET507", # superfluous-else-continue + "RET508", # superfluous-else-break "RSE102", # unnecessary-paren-on-raise-exception "RUF002", # ambiguous-unicode-character-docstring "RUF003", # ambiguous-unicode-character-comment @@ -242,7 +237,6 @@ ignore = [ "RUF013", # unnecessary-iterable-allocation-for-first-element "RUF015", # unnecessary-iterable-allocation-for-first-element "RUF019", # unnecessary-key-check - "RUF021", # parenthesize-chained-operators "RUF027", # missing-f-string-syntax "RUF100", # unused-noqa "S101", #assert @@ -292,7 +286,6 @@ ignore = [ "UP030", # format-literals "UP031", # printf-string-formatting "UP032", # f-string - "UP034", # extraneous-parentheses "UP036", # outdated-version-block "W605", # invalid-escape-sequence "YTT204", # sys-version-info-minor-cmp-int @@ -301,13 +294,16 @@ ignore = [ [tool.ruff.lint.per-file-ignores] # See https://docs.astral.sh/ruff/settings/#lint_per-file-ignores +# "A005", # builtin-module-shadowing # "INT002", # f-string-in-get-text-func-call # "INT001", # format-in-get-text-func-call # "INT003", # printf-in-get-text-func-call +# "PLW0108", # unnecessary-lambda # Ignore `E402` (import violations) in all `__init__.py` files "*/testsuite/**.py" = ["PT009", "PT027"] "__init__.py" = ["E402"] "general/g.parser/test.py" = ["INT003"] +"gui/**" = ["PLW0108"] # See https://github.com/OSGeo/grass/issues/4124 "gui/wxpython/animation/dialogs.py" = ["INT002"] "gui/wxpython/animation/temporal_manager.py" = ["INT003"] "gui/wxpython/core/debug.py" = ["INT002"] @@ -324,6 +320,7 @@ ignore = [ "gui/wxpython/iclass/dialogs.py" = ["INT003"] "gui/wxpython/iclass/frame.py" = ["FLY002", "INT003"] "gui/wxpython/iclass/plots.py" = ["INT003"] +"gui/wxpython/iclass/statistics.py" = ["A005"] "gui/wxpython/image2target/ii2t_gis_set.py" = ["INT003"] "gui/wxpython/iscatt/controllers.py" = ["INT003"] "gui/wxpython/iscatt/dialogs.py" = ["INT003"] @@ -350,6 +347,7 @@ ignore = [ "gui/wxpython/web_services/dialogs.py" = ["INT003"] "gui/wxpython/web_services/widgets.py" = ["INT003"] "gui/wxpython/wxgui.py" = ["INT002"] +"gui/wxpython/wxplot/profile.py" = ["A005"] "lib/imagery/testsuite/test_imagery_sigsetfile.py" = ["FURB152"] "lib/init/grass.py" = ["INT003"] "python/grass/__init__.py" = ["PYI056"] @@ -362,11 +360,13 @@ ignore = [ "python/grass/jupyter/tests/reprojection_renderer_test.py" = ["PT013"] "python/grass/jupyter/testsuite/interactivemap_test.py" = ["PGH004"] "python/grass/jupyter/testsuite/map_test.py" = ["PGH004"] +"python/grass/pydispatch/signal.py" = ["A005"] "python/grass/pygrass/raster/category.py" = ["INT002"] "python/grass/pygrass/vector/__init__.py" = ["INT003"] "python/grass/pygrass/vector/geometry.py" = ["PYI024"] "python/grass/pygrass/vector/sql.py" = ["FLY002"] "python/grass/pygrass/vector/testsuite/test_table.py" = ["PLW0108"] +"python/grass/script/array.py" = ["A005"] "python/grass/script/raster.py" = ["INT003"] "python/grass/temporal/abstract_space_time_dataset.py" = ["INT003"] "python/grass/temporal/aggregation.py" = ["INT003"] diff --git a/python/grass/app/runtime.py b/python/grass/app/runtime.py index 48ff8cde92b..5da00bae0df 100644 --- a/python/grass/app/runtime.py +++ b/python/grass/app/runtime.py @@ -28,16 +28,35 @@ def get_grass_config_dir(major_version, minor_version, env): Determines path of GRASS GIS user configuration directory. """ - # The code is in sync with grass.app.runtime (but not the same). + if env.get("GRASS_CONFIG_DIR"): + # use GRASS_CONFIG_DIR environmental variable is defined + env_dirname = "GRASS_CONFIG_DIR" + else: + env_dirname = "APPDATA" if WINDOWS else "HOME" + + config_dir = env.get(env_dirname) + if config_dir is None: + raise RuntimeError( + f"The {env_dirname} variable is not set, ask your operating" + " system support" + ) + + if not os.path.isdir(config_dir): + raise NotADirectoryError( + f"The {env_dirname} variable points to directory which does" + " not exist, ask your operating system support" + ) + if WINDOWS: config_dirname = f"GRASS{major_version}" - return os.path.join(env.get("APPDATA"), config_dirname) elif MACOS: - version = f"{major_version}.{minor_version}" - return os.path.join(env.get("HOME"), "Library", "GRASS", version) + config_dirname = os.path.join( + "Library", "GRASS", f"{major_version}.{minor_version}" + ) else: config_dirname = f".grass{major_version}" - return os.path.join(env.get("HOME"), config_dirname) + + return os.path.join(config_dir, config_dirname) def append_left_main_executable_paths(paths, install_path): diff --git a/python/grass/grassdb/manage.py b/python/grass/grassdb/manage.py index 28c4a87555b..77ac52e0823 100644 --- a/python/grass/grassdb/manage.py +++ b/python/grass/grassdb/manage.py @@ -173,7 +173,7 @@ def resolve_mapset_path(path, location=None, mapset=None) -> MapsetPath: from grass.grassdb.checks import is_mapset_valid if not is_mapset_valid(path) and is_mapset_valid(path / default_mapset): - path = path / default_mapset + path /= default_mapset parts = path.parts if len(parts) < 3: raise ValueError( diff --git a/python/grass/gunittest/reporters.py b/python/grass/gunittest/reporters.py index 45b1c386b27..bf82c768509 100644 --- a/python/grass/gunittest/reporters.py +++ b/python/grass/gunittest/reporters.py @@ -40,7 +40,7 @@ def keyvalue_to_text(keyvalue, sep="=", vsep="\n", isep=",", last_vertical=None) items.append("{key}{sep}{value}".format(key=key, sep=sep, value=value)) text = vsep.join(items) if last_vertical: - text = text + vsep + text += vsep return text diff --git a/python/grass/gunittest/runner.py b/python/grass/gunittest/runner.py index f1758e2e08b..b99c6704c89 100644 --- a/python/grass/gunittest/runner.py +++ b/python/grass/gunittest/runner.py @@ -164,7 +164,7 @@ def stopTestRun(self): self.printErrors() self.stream.writeln(self.separator2) run = self.testsRun - self.stream.write("Ran %d test%s" % (run, run != 1 and "s" or "")) + self.stream.write("Ran %d test%s" % (run, (run != 1 and "s") or "")) if self.time_taken: self.stream.write(" in %.3fs" % (self.time_taken)) self.stream.writeln() diff --git a/python/grass/imaging/images2gif.py b/python/grass/imaging/images2gif.py index ba681dca77d..a4dc2dc0824 100644 --- a/python/grass/imaging/images2gif.py +++ b/python/grass/imaging/images2gif.py @@ -471,7 +471,7 @@ def writeGifToFile(self, fp, images, durations, loops, xys, disposes): fp.write(d) # Prepare for next round - frames = frames + 1 + frames += 1 fp.write(";") # end gif return frames @@ -857,14 +857,14 @@ def altersingle(self, alpha, i, b, g, r): def geta(self, alpha, rad): try: - return self.a_s[(alpha, rad)] + return self.a_s[alpha, rad] except KeyError: length = rad * 2 - 1 mid = length / 2 q = np.array(list(range(mid - 1, -1, -1)) + list(range(-1, mid))) a = alpha * (rad * rad - q * q) / (rad * rad) a[mid] = 0 - self.a_s[(alpha, rad)] = a + self.a_s[alpha, rad] = a return a def alterneigh(self, alpha, rad, i, b, g, r): diff --git a/python/grass/imaging/images2swf.py b/python/grass/imaging/images2swf.py index e98d567d83c..0a08ed9d46d 100644 --- a/python/grass/imaging/images2swf.py +++ b/python/grass/imaging/images2swf.py @@ -65,6 +65,7 @@ of a watermark in the upper left corner. """ +from __future__ import annotations import os import zlib @@ -224,7 +225,7 @@ def intToUint8(i): return int(i).to_bytes(1, "little") -def intToBits(i, n=None): +def intToBits(i: int, n: int | None = None) -> BitArray: """convert int to a string of bits (0's and 1's in a string), pad to n elements. Convert back using int(ss,2).""" ii = i @@ -233,7 +234,7 @@ def intToBits(i, n=None): bb = BitArray() while ii > 0: bb += str(ii % 2) - ii = ii >> 1 + ii >>= 1 bb.Reverse() # justify @@ -295,7 +296,7 @@ def getTypeAndLen(bb): return type, L, L2 -def signedIntToBits(i, n=None): +def signedIntToBits(i: int, n: int | None = None) -> BitArray: """convert signed int to a string of bits (0's and 1's in a string), pad to n elements. Negative numbers are stored in 2's complement bit patterns, thus positive numbers always start with a 0. @@ -311,7 +312,7 @@ def signedIntToBits(i, n=None): bb = BitArray() while ii > 0: bb += str(ii % 2) - ii = ii >> 1 + ii >>= 1 bb.Reverse() # justify @@ -489,7 +490,7 @@ def ProcessTag(self): for i in range(3): clr = self.rgb[i] if isinstance(clr, float): - clr = clr * 255 + clr *= 255 bb += intToUint8(clr) self.bytes = bb diff --git a/python/grass/jupyter/baseseriesmap.py b/python/grass/jupyter/baseseriesmap.py index db915297e9c..634e172f3c5 100644 --- a/python/grass/jupyter/baseseriesmap.py +++ b/python/grass/jupyter/baseseriesmap.py @@ -1,3 +1,18 @@ +# +# AUTHOR(S): Riya Saxena <29riyasaxena AT gmail> +# +# PURPOSE: This module provides the base class for interactive visualizations +# used by `TimeSeriesMap` and `SeriesMap` in Jupyter Notebooks. It +# includes methods for rendering visualizations and creating interactive +# sliders to navigate through time-series or series data, while reducing +# redundancy and enhancing functionality. +# +# COPYRIGHT: (C) 2024 Riya Saxena, and by the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. + """Base class for SeriesMap and TimeSeriesMap""" import os @@ -5,10 +20,12 @@ import tempfile import weakref import shutil +import multiprocessing import grass.script as gs from .map import Map +from .utils import get_number_of_cores class BaseSeriesMap: @@ -87,7 +104,7 @@ def _render_baselayers(self, img): for grass_module, kwargs in self._base_layer_calls: img.run(grass_module, **kwargs) - def _render(self): + def _render(self, tasks): """ Renders the base image for the dataset. @@ -121,6 +138,14 @@ def _render(self): self._render_baselayers(img) # Render layers in respective classes + cores = get_number_of_cores(len(tasks), env=self._env) + with multiprocessing.Pool(processes=cores) as pool: + results = pool.starmap(self._render_worker, tasks) + + for i, filename in results: + self._base_filename_dict[i] = filename + + self._layers_rendered = True def show(self, slider_width=None): """Create interactive timeline slider. diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 561b2cdfbb2..b6a5ccfb3e8 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -1,6 +1,7 @@ # # AUTHOR(S): Caitlin Haedrich # Anna Petrasova +# Riya Saxena <29riyasaxena AT gmail> # # PURPOSE: This module contains functions for interactive visualizations # in Jupyter Notebooks. @@ -12,16 +13,22 @@ # for details. """Interactive visualizations map with folium or ipyleaflet""" - +import os import base64 import json from pathlib import Path from .reprojection_renderer import ReprojectionRenderer + from .utils import ( get_region_bounds_latlon, reproject_region, update_region, get_location_proj_string, + save_vector, + get_region, + query_raster, + query_vector, + reproject_latlon, ) @@ -245,6 +252,7 @@ def __init__( """ self._ipyleaflet = None self._folium = None + self._ipywidgets = None def _import_folium(error): try: @@ -282,17 +290,27 @@ def _import_ipyleaflet(error): if self._ipyleaflet: import ipywidgets as widgets # pylint: disable=import-outside-toplevel + + self._ipywidgets = widgets import xyzservices # pylint: disable=import-outside-toplevel # Store height and width self.width = width self.height = height + self._controllers = {} + + # Store vector and raster name + self.raster_name = [] + self.vector_name = [] + + # Store Region + self.region = None if self._ipyleaflet: basemap = xyzservices.providers.query_name(tiles) if API_key and basemap.get("accessToken"): basemap["accessToken"] = API_key - layout = widgets.Layout(width=f"{width}px", height=f"{height}px") + layout = self._ipywidgets.Layout(width=f"{width}px", height=f"{height}px") self.map = self._ipyleaflet.Map( basemap=basemap, layout=layout, scroll_wheel_zoom=True ) @@ -321,6 +339,7 @@ def add_vector(self, name, title=None, **kwargs): :param str title: vector name for layer control :**kwargs: keyword arguments passed to GeoJSON overlay """ + self.vector_name.append(name) Vector(name, title=title, renderer=self._renderer, **kwargs).add_to(self.map) def add_raster(self, name, title=None, **kwargs): @@ -339,6 +358,7 @@ def add_raster(self, name, title=None, **kwargs): :param str title: raster name for layer control :**kwargs: keyword arguments passed to image overlay """ + self.raster_name.append(name) Raster(name, title=title, renderer=self._renderer, **kwargs).add_to(self.map) def add_layer_control(self, **kwargs): @@ -353,111 +373,78 @@ def add_layer_control(self, **kwargs): else: self.layer_control_object = self._ipyleaflet.LayersControl(**kwargs) - def draw_computational_region(self): - """ - Allow users to draw the computational region and modify it. + def setup_drawing_interface(self): + """Sets up the drawing interface for users + to interactively draw and manage geometries on the map. + + This includes creating a toggle button to activate the drawing mode, and + instantiating an InteractiveDrawController to handle the drawing functionality. """ - import ipywidgets as widgets # pylint: disable=import-outside-toplevel + return self._create_toggle_button( + icon="pencil", + tooltip=_("Click to draw geometries"), + controller_class=InteractiveDrawController, + ) - region_mode_button = widgets.ToggleButton( + def setup_computational_region_interface(self): + """Sets up the interface for users to draw and + modify the computational region on the map. + + This includes creating a toggle button to activate the + region editing mode, and instantiating an InteractiveRegionController to + handle the region selection and modification functionality. + """ + return self._create_toggle_button( icon="square-o", - description="", - value=False, - tooltip="Click to show and edit computational region", - layout=widgets.Layout(width="40px", margin="0px 0px 0px 0px"), + tooltip=_("Click to show and edit computational region"), + controller_class=InteractiveRegionController, ) - save_button = widgets.Button( - description="Update region", - tooltip="Click to update region", - disabled=True, - ) - bottom_output_widget = widgets.Output( - layout={ - "width": "100%", - "max_height": "300px", - "overflow": "auto", - "display": "none", - } - ) + def setup_query_interface(self): + """Sets up the query button interface. - changed_region = {} - save_button_control = None - - def update_output(region): - with bottom_output_widget: - bottom_output_widget.clear_output() - print( - _( - "Region changed to: n={n}, s={s}, e={e}, w={w} " - "nsres={nsres} ewres={ewres}" - ).format(**region) - ) - - def on_rectangle_change(value): - save_button.disabled = False - bottom_output_widget.layout.display = "none" - latlon_bounds = value["new"][0] - changed_region["north"] = latlon_bounds[2]["lat"] - changed_region["south"] = latlon_bounds[0]["lat"] - changed_region["east"] = latlon_bounds[2]["lng"] - changed_region["west"] = latlon_bounds[0]["lng"] - - def toggle_region_mode(change): - nonlocal save_button_control - - if change["new"]: - region_bounds = get_region_bounds_latlon() - self.region_rectangle = self._ipyleaflet.Rectangle( - bounds=region_bounds, - color="red", - fill_color="red", - fill_opacity=0.5, - draggable=True, - transform=True, - rotation=False, - name="Computational region", - ) - self.region_rectangle.observe(on_rectangle_change, names="locations") - self.map.fit_bounds(region_bounds) - self.map.add(self.region_rectangle) - - save_button_control = self._ipyleaflet.WidgetControl( - widget=save_button, position="topright" - ) - self.map.add(save_button_control) - else: - if self.region_rectangle: - self.region_rectangle.transform = False - self.map.remove(self.region_rectangle) - self.region_rectangle = None - - save_button.disabled = True - - if save_button_control: - self.map.remove(save_button_control) - bottom_output_widget.layout.display = "none" - - def save_region(_change): - from_proj = "+proj=longlat +datum=WGS84 +no_defs" - to_proj = get_location_proj_string() - reprojected_region = reproject_region(changed_region, from_proj, to_proj) - new = update_region(reprojected_region) - bottom_output_widget.layout.display = "block" - update_output(new) - - region_mode_button.observe(toggle_region_mode, names="value") - save_button.on_click(save_region) - - region_mode_control = self._ipyleaflet.WidgetControl( - widget=region_mode_button, position="topright" + This includes creating a toggle button to activate the + query mode, and instantiating an InteractiveQueryController to + handle the user query. + """ + return self._create_toggle_button( + icon="info", + tooltip=_("Click to query raster and vector maps"), + controller_class=InteractiveQueryController, ) - self.map.add(region_mode_control) - output_control = self._ipyleaflet.WidgetControl( - widget=bottom_output_widget, position="bottomleft" + def _create_toggle_button(self, icon, tooltip, controller_class): + button = self._ipywidgets.ToggleButton( + icon=icon, + value=False, + tooltip=tooltip, + description="", + # layout=self._ipywidgets.Layout( + # width="43px", margin="0px", #border="2px solid darkgrey" + # ), ) - self.map.add(output_control) + controller = controller_class( + map_object=self.map, + ipyleaflet=self._ipyleaflet, + ipywidgets=self._ipywidgets, + toggle_button=button, + rasters=self.raster_name, + vectors=self.vector_name, + width=self.width, + ) + self._controllers[button] = controller + button.observe(self._toggle_mode, names="value") + return button + + def _toggle_mode(self, change): + if change["new"]: + for button, controller in self._controllers.items(): + if button is not change["owner"]: + button.value = False + controller.deactivate() + self._controllers[change["owner"]].activate() + else: + self._controllers[change["owner"]].deactivate() def show(self): """This function returns a folium figure or ipyleaflet map object @@ -466,7 +453,18 @@ def show(self): If map has layer control enabled, additional layers cannot be added after calling show().""" if self._ipyleaflet: - self.draw_computational_region() + toggle_buttons = [ + self.setup_query_interface(), + self.setup_computational_region_interface(), + self.setup_drawing_interface(), + ] + button_box = self._ipywidgets.HBox( + toggle_buttons, layout=self._ipywidgets.Layout(width="150px") + ) + self.map.add( + self._ipyleaflet.WidgetControl(widget=button_box, position="topright") + ) + self.map.fit_bounds(self._renderer.get_bbox()) if not self.layer_control_object: @@ -490,3 +488,357 @@ def save(self, filename): :param str filename: name of html file """ self.map.save(filename) + + +class InteractiveRegionController: + """A controller for interactive region selection on a map. + + Attributes: + map: The map object. + region_rectangle: The rectangle representing the selected region. + _ipyleaflet: The ipyleaflet module. + _ipywidgets: The ipywidgets module. + save_button: The button to save the selected region. + bottom_output_widget: The output widget to display the selected region. + changed_region (dict): The dictionary to store the changed region. + """ + + def __init__( + self, map_object, ipyleaflet, ipywidgets, **kwargs + ): # pylint: disable=unused-argument + """Initializes the InteractiveRegionController. + + :param map_object: The map object. + :param ipyleaflet: The ipyleaflet module. + :param ipywidgets: The ipywidgets module. + """ + self.map = map_object + self.region_rectangle = None + self._ipyleaflet = ipyleaflet + self._ipywidgets = ipywidgets + + self.save_button = self._ipywidgets.Button( + description="Update region", + tooltip="Click to update region", + disabled=True, + ) + self.bottom_output_widget = self._ipywidgets.Output( + layout={ + "width": "100%", + "max_height": "300px", + "overflow": "auto", + "display": "none", + } + ) + self.changed_region = {} + self.save_button_control = None + self.save_button.on_click(self._save_region) + + output_control = self._ipyleaflet.WidgetControl( + widget=self.bottom_output_widget, position="bottomleft" + ) + self.map.add(output_control) + + def _update_output(self, region): + """Updates the output widget with the selected region. + + :param dict region: The selected region. + """ + with self.bottom_output_widget: + self.bottom_output_widget.clear_output() + print( + _( + "Region changed to: n={n}, s={s}, e={e}, w={w} " + "nsres={nsres} ewres={ewres}" + ).format(**region) + ) + + def _on_rectangle_change(self, value): + """Handles the change event of the rectangle. + + :param dict value: The changed value. + """ + self.save_button.disabled = False + self.bottom_output_widget.layout.display = "none" + latlon_bounds = value["new"][0] + self.changed_region["north"] = latlon_bounds[2]["lat"] + self.changed_region["south"] = latlon_bounds[0]["lat"] + self.changed_region["east"] = latlon_bounds[2]["lng"] + self.changed_region["west"] = latlon_bounds[0]["lng"] + + def activate(self): + """Activates the interactive region selection.""" + region_bounds = get_region_bounds_latlon() + self.region_rectangle = self._ipyleaflet.Rectangle( + bounds=region_bounds, + color="red", + fill_opacity=0, + opacity=0.5, + draggable=True, + transform=True, + rotation=False, + name="Computational region", + ) + self.region_rectangle.observe(self._on_rectangle_change, names="locations") + self.map.fit_bounds(region_bounds) + self.map.add(self.region_rectangle) + + self.save_button_control = self._ipyleaflet.WidgetControl( + widget=self.save_button, position="topright" + ) + self.map.add(self.save_button_control) + + def deactivate(self): + """Deactivates the interactive region selection.""" + if self.region_rectangle: + self.region_rectangle.transform = False + self.map.remove(self.region_rectangle) + self.region_rectangle = None + + if ( + hasattr(self, "save_button_control") + and self.save_button_control in self.map.controls + ): + self.map.remove(self.save_button_control) + + self.save_button.disabled = True + self.bottom_output_widget.layout.display = "none" + + def _save_region(self, _change): + """Saves the selected region. + + :param _change:Not used. + """ + from_proj = "+proj=longlat +datum=WGS84 +no_defs" + to_proj = get_location_proj_string() + reprojected_region = reproject_region(self.changed_region, from_proj, to_proj) + new = update_region(reprojected_region) + self.bottom_output_widget.layout.display = "block" + self._update_output(new) + + +class InteractiveDrawController: + """A controller for interactive drawing on a map. + + Attributes: + map: The map object. + _ipyleaflet: The ipyleaflet module. + draw_control: The draw control. + drawn_geometries: The list of drawn geometries. + self.vector_layers: List of vector layers + geo_json_layers: The dictionary of GeoJSON layers. + save_button_control: The save button control. + toggle_button: The toggle button activating/deactivating drawing. + """ + + def __init__( + self, map_object, ipyleaflet, ipywidgets, toggle_button, vectors, **kwargs + ): # pylint: disable=unused-argument + """Initializes the InteractiveDrawController. + + :param map_object: The map object. + :param ipyleaflet: The ipyleaflet module. + :param ipywidgets: The ipywidgets module. + :param toggle_button: The toggle button activating/deactivating drawing. + :param vectors: List of vector layers. + """ + self.map = map_object + self._ipyleaflet = ipyleaflet + self._ipywidgets = ipywidgets + self.toggle_button = toggle_button + self.vector_layers = vectors + self.draw_control = self._ipyleaflet.DrawControl(edit=False, remove=False) + self.drawn_geometries = [] + self.geo_json_layers = {} + self.save_button_control = None + + self.name_input = self._ipywidgets.Text( + description=_("New vector map name:"), + style={"description_width": "initial"}, + layout=self._ipywidgets.Layout(width="80%", margin="1px 1px 1px 5px"), + ) + + self.save_button = self._ipywidgets.Button( + description=_("Save"), + layout=self._ipywidgets.Layout(width="20%", margin="1px 1px 1px 1px"), + ) + + self.save_button.on_click(self._save_geometries) + + def activate(self): + """Activates the interactive drawing.""" + self.map.add_control(self.draw_control) + self.draw_control.on_draw(self._handle_draw) + self._show_interface() + + def deactivate(self): + """Deactivates the interactive drawing.""" + self.draw_control.clear() + if self.draw_control in self.map.controls: + self.map.remove(self.draw_control) + self.drawn_geometries.clear() + self._hide_interface() + + def _handle_draw(self, _, action, geo_json): + """Handles the draw event. + + :param str action: The action type. + :param dict geo_json: The GeoJSON data. + """ + if action == "created": + self.drawn_geometries.append(geo_json) + print(f"Geometry created: {geo_json}") + + def _show_interface(self): + """Shows the interface for saving the drawn geometries.""" + hbox_layout = self._ipywidgets.Layout( + display="flex", + flex_flow="row", + align_items="stretch", + width="300px", + justify_content="space-between", + ) + self.name_input.value = "" + self.save_button_control = self._ipyleaflet.WidgetControl( + widget=self._ipywidgets.HBox( + [self.name_input, self.save_button], layout=hbox_layout + ), + position="topright", + ) + + self.map.add_control(self.save_button_control) + + def _hide_interface(self): + """Hides the interface for saving the drawn geometries.""" + if self.save_button_control: + self.map.remove_control(self.save_button_control) + self.save_button_control = None + + def _save_geometries(self, _b): + """Saves the drawn geometries. + + :param _b: Not used. + """ + name = self.name_input.value + if name and self.drawn_geometries: + for geometry in self.drawn_geometries: + geometry["properties"]["name"] = name + geo_json = { + "type": "FeatureCollection", + "features": self.drawn_geometries, + } + save_vector(name, geo_json) + geo_json_layer = self._ipyleaflet.GeoJSON(data=geo_json, name=name) + self.geo_json_layers[name] = geo_json_layer + self.vector_layers.append(name) + self.map.add_layer(geo_json_layer) + self.deactivate() + self.toggle_button.value = False + + +class InteractiveQueryController: + """A controller for interactive querying on a map. + + Attributes: + map: The ipyleaflet.Map object. + _ipyleaflet: The ipyleaflet module. + _ipywidgets: The ipywidgets module. + raster_name: The name of the raster layer. + vector_name: The name of the vector layer. + width: The width of the map. + query_control: The query control. + + """ + + def __init__( + self, map_object, ipyleaflet, ipywidgets, rasters, vectors, width, **kwargs + ): # pylint: disable=unused-argument + """Initializes the InteractiveQueryController. + + :param map: The map object. + :param ipyleaflet: The ipyleaflet module. + :param ipywidgets: The ipywidgets module. + """ + self.map = map_object + self._ipyleaflet = ipyleaflet + self._ipywidgets = ipywidgets + self.raster_name = rasters + self.vector_name = vectors + self.width = width + self.query_control = None + + def activate(self): + """Activates the interactive querying.""" + self.map.on_interaction(self.handle_interaction) + self.map.default_style = {"cursor": "crosshair"} + + def deactivate(self): + """Deactivates the interactive querying.""" + self.map.default_style = {"cursor": "default"} + self.map.on_interaction(self.handle_interaction, remove=True) + self.clear_popups() + + def handle_interaction(self, **kwargs): + """Handles the map interaction event. + + :param kwargs: The event arguments. + """ + if kwargs.get("type") != "click": + return + + lonlat = kwargs.get("coordinates") + reprojected_coordinates = reproject_latlon(lonlat) + raster_output = self.query_raster(reprojected_coordinates) + vector_output = self.query_vector(reprojected_coordinates) + self.show_popup(lonlat, raster_output + vector_output) + + def query_raster(self, coordinates): + """Queries the raster layer. + + :param coordinates: The coordinates. + :return: The raster output. + """ + return query_raster(coordinates, self.raster_name) + + def query_vector(self, coordinates): + """Queries the vector layer. + + :param coordinates: The coordinates. + :return: The vector output. + """ + region = get_region(env=os.environ.copy()) + return query_vector( + coordinates, + self.vector_name, + 10.0 * ((region["east"] - region["west"]) / self.width), + ) + + def show_popup(self, lonlat, message_content): + """Shows a popup with the query result. + + :param lonlat: The latitude and longitude coordinates. + :param message_content: The message content. + """ + scrollable_container = self._ipywidgets.HTML( + value=( + "
" + f"{message_content}" + "
" + ) + ) + + popup = self._ipyleaflet.Popup( + location=lonlat, + child=scrollable_container, + close_button=False, + auto_close=True, + close_on_escape_key=False, + ) + self.map.add(popup) + + def clear_popups(self): + """Clears the popups.""" + for item in reversed(list(self.map.layers)): + if isinstance(item, self._ipyleaflet.Popup): + self.map.remove(item) diff --git a/python/grass/jupyter/seriesmap.py b/python/grass/jupyter/seriesmap.py index 9e470babb70..8615ad488fb 100644 --- a/python/grass/jupyter/seriesmap.py +++ b/python/grass/jupyter/seriesmap.py @@ -1,11 +1,12 @@ # MODULE: grass.jupyter.seriesmap # # AUTHOR(S): Caitlin Haedrich +# Riya Saxena <29riyasaxena AT gmail> # # PURPOSE: This module contains functions for visualizing series of rasters in # Jupyter Notebooks # -# COPYRIGHT: (C) 2022 Caitlin Haedrich, and by the GRASS Development Team +# COPYRIGHT: (C) 2022-2024 Caitlin Haedrich, and by the GRASS Development Team # # This program is free software under the GNU General Public # License (>=v2). Read the file COPYING that comes with GRASS @@ -135,39 +136,35 @@ def add_names(self, names): self._labels = names self._indices = list(range(len(self._labels))) + def _render_worker(self, i): + """Function to render a single layer.""" + filename = os.path.join(self._tmpdir.name, f"{i}.png") + shutil.copyfile(self.base_file, filename) + img = Map( + width=self._width, + height=self._height, + filename=filename, + use_region=True, + env=self._env, + read_file=True, + ) + for grass_module, kwargs in self._base_calls[i]: + img.run(grass_module, **kwargs) + return i, filename + def render(self): """Renders image for each raster in series. Save PNGs to temporary directory. Must be run before creating a visualization (i.e. show or save). """ - self._render() if not self._baseseries_added: raise RuntimeError( "Cannot render series since none has been added." "Use SeriesMap.add_rasters() or SeriesMap.add_vectors()" ) - - # Render each layer - for i in range(self.baseseries): - # Create file - filename = os.path.join(self._tmpdir.name, f"{i}.png") - # Copying the base_file ensures that previous results are overwritten - shutil.copyfile(self.base_file, filename) - self._base_filename_dict[i] = filename - # Render image - img = Map( - width=self._width, - height=self._height, - filename=filename, - use_region=True, - env=self._env, - read_file=True, - ) - for grass_module, kwargs in self._base_calls[i]: - img.run(grass_module, **kwargs) - - self._layers_rendered = True + tasks = [(i,) for i in range(self.baseseries)] + self._render(tasks) def save( self, diff --git a/python/grass/jupyter/testsuite/interactivemap_test.py b/python/grass/jupyter/testsuite/interactivemap_test.py index 80836cdf31e..6b8a7379548 100644 --- a/python/grass/jupyter/testsuite/interactivemap_test.py +++ b/python/grass/jupyter/testsuite/interactivemap_test.py @@ -98,6 +98,22 @@ def test_save_as_html(self): interactive_map.save(filename) self.assertFileExists(filename) + @unittest.skipIf(not can_import_ipyleaflet(), "Cannot import ipyleaflet") + def test_query_button(self): + # Create InteractiveMap with ipyleaflet backend + interactive_map = gj.InteractiveMap(map_backend="ipyleaflet") + interactive_map.add_raster("elevation") + interactive_map.add_vector("roadsmajor") + interactive_map.add_query_button() + self.assertIsNotNone(interactive_map.map) + self.assertTrue(interactive_map.query_mode is False) + # Toggle query button to activate + interactive_map.query_mode = True + self.assertTrue(interactive_map.query_mode) + # Toggle query button to deactivate + interactive_map.query_mode = False + self.assertFalse(interactive_map.query_mode) + @unittest.skipIf(not can_import_ipyleaflet(), "Cannot import ipyleaflet") def test_draw_computational_region(self): """Test the draw_computational_region method.""" diff --git a/python/grass/jupyter/timeseriesmap.py b/python/grass/jupyter/timeseriesmap.py index 2dc0b4ede56..3ac94bea93e 100644 --- a/python/grass/jupyter/timeseriesmap.py +++ b/python/grass/jupyter/timeseriesmap.py @@ -1,11 +1,12 @@ # MODULE: grass.jupyter.timeseriesmap # # AUTHOR(S): Caitlin Haedrich +# Riya Saxena <29riyasaxena AT gmail> # # PURPOSE: This module contains functions for visualizing raster and vector # space-time datasets in Jupyter Notebooks # -# COPYRIGHT: (C) 2022 Caitlin Haedrich, and by the GRASS Development Team +# COPYRIGHT: (C) 2022-2024 Caitlin Haedrich, and by the GRASS Development Team # # This program is free software under the GNU General Public # License (>=v2). Read the file COPYING that comes with GRASS @@ -281,48 +282,36 @@ def _render_layer(self, layer, filename): if self._legend: self._render_legend(img) - def render(self): - """Renders image for each time-step in space-time dataset. - - Save PNGs to temporary directory. Must be run before creating a visualization - (i.e. show or save). Can be time-consuming to run with large - space-time datasets. - """ + def _render_worker(self, date, layer, filename): + """Function to render a single layer.""" + shutil.copyfile(self.base_file, filename) + if layer == "None": + self._render_blank_layer(filename) + else: + self._render_layer(layer, filename) + return date, filename - self._render() + def render(self): + """Renders image for each time-step in space-time dataset.""" if not self._baseseries_added: raise RuntimeError( "Cannot render space time dataset since none has been added." - "Use TimeSeriesMap.add_raster_series() or " + " Use TimeSeriesMap.add_raster_series() or " "TimeSeriesMap.add_vector_series() to add dataset" ) # Create name for empty layers - # Random name needed to avoid potential conflict with layer names - # A new random_name_none is created each time the render function is run, - # and any existing random_name_none file will be ignored random_name_none = gs.append_random("none", 8) + ".png" - # Render each layer + # Prepare tasks with tuples + tasks = [] for date, layer in self._date_layer_dict.items(): if layer == "None": - # Create file filename = os.path.join(self._tmpdir.name, random_name_none) - self._base_filename_dict[date] = filename - # Render blank layer if it hasn't been done already - if not os.path.exists(filename): - shutil.copyfile(self.base_file, filename) - self._render_blank_layer(filename) else: - # Create file filename = os.path.join(self._tmpdir.name, f"{layer}.png") - # Copying the base_file ensures that previous results are overwritten - shutil.copyfile(self.base_file, filename) - self._base_filename_dict[date] = filename - # Render image - self._render_layer(layer, filename) - - self._layers_rendered = True + tasks.append((date, layer, filename)) + self._render(tasks) def save( self, diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index 0556158896f..b06553d2d3b 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -1,15 +1,21 @@ # # AUTHOR(S): Caitlin Haedrich +# Riya Saxena <29riyasaxena AT gmail> # # PURPOSE: This module contains utility functions for InteractiveMap. # -# COPYRIGHT: (C) 2021-2022 Caitlin Haedrich, and by the GRASS Development Team +# COPYRIGHT: (C) 2021-2024 Caitlin Haedrich, and by the GRASS Development Team # # This program is free software under the GNU General Public # License (>=v2). Read the file COPYING that comes with GRASS # for details. """Utility functions warpping existing processes in a suitable way""" +import tempfile +import json +import os +import multiprocessing + from pathlib import Path import grass.script as gs @@ -87,6 +93,216 @@ def reproject_region(region, from_proj, to_proj): return region +def reproject_latlon(coord): + """Reproject coordinates + + :param coord: coordinates given as tuple (latitude, longitude) + :return: reprojected coordinates (returned as tuple) + """ + # Prepare the input coordinate string + coord_str = f"{coord[1]} {coord[0]}\n" + + # Start the m.proj command + proc = gs.start_command( + "m.proj", + input="-", + flags="i", + separator=",", + stdin=gs.PIPE, + stdout=gs.PIPE, + stderr=gs.PIPE, + ) + + proc.stdin.write(gs.encode(coord_str)) + proc.stdin.close() + proc.stdin = None + proj_output, _ = proc.communicate() + + output = gs.decode(proj_output).splitlines() + east, north, elev = map(float, output[0].split(",")) + + return east, north, elev + + +def _style_table(html_content): + """ + Use to style table displayed in popup. + + :param html_content: HTML content to be displayed + + :return str: formatted HTML content + """ + css = """ + + """ + return f"{css}{html_content}" + + +def _format_nested_table(attributes): + """ + Format nested attributes into an HTML table row. + + :param attributes: Dictionary of nested attributes to format. + + :return: str: HTML formatted string containing rows for each non-empty attribute. + """ + nested_table = "" + for sub_key, sub_value in attributes.items(): + if sub_value: + nested_table += f""" + + {sub_key} + {sub_value} + + """ + return nested_table + + +def _format_regular_output(items): + """ + Format attributes into an HTML table. + + :param items: List of key-value pairs (tuples) to process. + + :return: str: HTML formatted string containing rows for specified attributes. + """ + regular_output = "" + for key, value in items: + if key in {"Category", "Layer"}: + regular_output += f""" + + {key} + {value} + + """ + return regular_output + + +def query_raster(coord, raster_list): + """ + Queries raster data at specified coordinates. + + :param coord: Coordinates given as a tuple (latitude, longitude). + :param list raster_list: List of raster names to query. + + :return: str: HTML formatted string containing the results of the raster queries. + """ + output_list = [""""""] + + for raster in raster_list: + raster_output = gs.raster.raster_what(map=raster, coord=coord) + + output = f""" + """ + + if raster in raster_output[0]: + if "value" in raster_output[0][raster]: + value = raster_output[0][raster]["value"] + output += f""" + + + + + """ + items = raster_output[0][raster].items() + formatted_output = _format_regular_output(items) + output += formatted_output + + output_list.append(output) + + if len(output_list) == 1: + return "" + + output_list.extend(("
Raster: {raster}
Value{value}
", "
")) + final_output = "".join(output_list) + return _style_table(final_output) + + +def _process_vector_output(vector, coord, distance): + """ + Process the output of a vector query. + + :param vector: Name of the vector map to query. + :param coord: Coordinates given as a tuple for querying. + :param distance: Distance within which to query the vector attributes. + + :return: str: HTML formatted string containing the vector output, including regular + attributes and any nested attribute tables. + """ + vector_output = gs.vector.vector_what(map=vector, coord=coord, distance=distance) + if len(vector_output[0]) <= 2: + return "" + + items = list(vector_output[0].items()) + attributes_output = "" + regular_output = _format_regular_output(items) + + for key, value in items: + if key == "Attributes" and isinstance(value, dict): + attributes_output = _format_nested_table(value) + + vector_html = f""" + + + Vector: {vector} + + + """ + return vector_html + attributes_output + regular_output + + +def query_vector(coord, vector_list, distance): + """ + Queries vector data at specified coordinates. + + :param coord: Coordinates given as a tuple (latitude, longitude). + :param list vector_list: List of vector names to query. + :param distance: Distance within which to query the vector attributes. + + :return: str: HTML formatted string containing the results of the vector queries. + """ + output_list = [""] + + for vector in vector_list: + vector_html = _process_vector_output(vector, coord, distance) + if vector_html: + output_list.append(vector_html) + + if len(output_list) == 1: + return "" + + output_list.extend(("
", "
")) + final_output = "".join(output_list) + return _style_table(final_output) + + def estimate_resolution(raster, mapset, location, dbase, env): """Estimates resolution of reprojected raster. @@ -201,8 +417,48 @@ def get_rendering_size(region, width, height, default_width=600, default_height= return (default_width, round(default_width * region_height / region_width)) +def save_vector(name, geo_json): + """Saves the user drawn vector. + + :param geo_json: name of the geojson file to be saved + :param name: name with which vector should be saved + """ + with tempfile.NamedTemporaryFile( + suffix=".geojson", delete=False, mode="w" + ) as temp_file: + temp_filename = temp_file.name + for each in geo_json["features"]: + each["properties"].clear() + json.dump(geo_json, temp_file) + gs.run_command("v.import", input=temp_filename, output=name) + os.remove(temp_filename) + + +def get_number_of_cores(requested, env=None): + """Get the number of cores to use for multiprocessing. + + :param int requested: Desired number of cores. + :param dict env: Optional process environment. + + :return int: Number of cores to use, constrained by system availability. + """ + nprocs = gs.gisenv(env).get("NPROCS") + if nprocs is not None: + return int(nprocs) + + try: + num_cores = len(os.sched_getaffinity(0)) + except AttributeError: + num_cores = multiprocessing.cpu_count() + return min(requested, max(1, num_cores - 1)) + + def get_region_bounds_latlon(): - """Gets the current computational region bounds in latlon.""" + """Gets the current computational region bounds in latlon. + + :return list of tuples: represent the southwest and northeast + corners of the region in (latitude, longitude) format. + """ region = gs.parse_command("g.region", flags="gbp") return [ (float(region["ll_s"]), float(region["ll_w"])), @@ -213,6 +469,7 @@ def get_region_bounds_latlon(): def update_region(region): """Updates the computational region bounds. + :param dict region: region dictionary :return: the new region """ current = gs.region() @@ -238,8 +495,7 @@ def save_gif( text_size=12, text_color="gray", ): - """ - Creates a GIF animation + """Creates a GIF animation param list input_files: list of paths to source param str output_filename: destination gif filename diff --git a/python/grass/pydispatch/PKG-INFO b/python/grass/pydispatch/PKG-INFO index 19752eec0fe..2ad96bdff66 100644 --- a/python/grass/pydispatch/PKG-INFO +++ b/python/grass/pydispatch/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: PyDispatcher -Version: 2.0.3 +Version: 2.0.8 Summary: Multi-producer-multi-consumer signal dispatching mechanism Home-page: http://pydispatcher.sourceforge.net Author: Mike C. Fletcher diff --git a/python/grass/pydispatch/__init__.py b/python/grass/pydispatch/__init__.py index 30f1af7fd21..beee0d6101e 100644 --- a/python/grass/pydispatch/__init__.py +++ b/python/grass/pydispatch/__init__.py @@ -16,10 +16,9 @@ PyDispatcher because it provides very general API which enables to implement Signal API, wide and robust functionality which makes implementation and use of Signals easier. - -PyDispatcher metadata: - -:version: 2.0.3 -:author: Patrick K. O'Brien -:license: BSD-style, see license.txt for details """ + +__version__ = "2.0.8" +__author__ = "Patrick K. O'Brien" +__maintainer__ = "Mike C. Fletcher" +__license__ = "BSD" diff --git a/python/grass/pydispatch/dispatcher.py b/python/grass/pydispatch/dispatcher.py index 0915629fdd9..4757acf36cf 100644 --- a/python/grass/pydispatch/dispatcher.py +++ b/python/grass/pydispatch/dispatcher.py @@ -27,11 +27,7 @@ """ import weakref -from grass.pydispatch import saferef, robustapply, errors - -__author__ = "Patrick K. O'Brien " -__cvsid__ = "Id: dispatcher.py,v 1.1 2010/03/30 15:45:55 mcfletch Exp" -__version__ = "Revision: 1.1" +from grass.pydispatch import errors, saferef, robustapply class _Parameter: @@ -469,7 +465,7 @@ def _removeOldBackRefs(senderkey, signal, receiver, receivers): found = 0 signals = connections.get(signal) if signals is not None: - for sig, recs in connections.get(signal, {}).iteritems(): + for sig, recs in connections.get(signal, {}).items(): if sig != signal: for rec in recs: if rec is oldReceiver: diff --git a/python/grass/pydispatch/robustapply.py b/python/grass/pydispatch/robustapply.py index 1c10bc917c6..a050f54341a 100644 --- a/python/grass/pydispatch/robustapply.py +++ b/python/grass/pydispatch/robustapply.py @@ -40,22 +40,51 @@ def function(receiver): return receiver, getattr(receiver, func_code), 0 +VAR_ARGS = 4 +VAR_NAMES = 8 + + def robustApply(receiver, *arguments, **named): - """Call receiver with arguments and an appropriate subset of named""" + """Call receiver with arguments and an appropriate subset of named + + The effect of this wrapper is to allow for specifying a large number + of parameters which may not exist in the final function via named + parameters, and have those parameters ignored in the final call. + """ receiver, codeObject, startIndex = function(receiver) - acceptable = codeObject.co_varnames[ - startIndex + len(arguments) : codeObject.co_argcount + has_varargs = bool(codeObject.co_flags & VAR_ARGS) + has_varnames = bool(codeObject.co_flags & VAR_NAMES) + + posonly_count = getattr(codeObject, "co_posonlyargcount", 0) + + posnamed_arguments = codeObject.co_varnames[posonly_count : codeObject.co_argcount] + named_onlyarguments = codeObject.co_varnames[ + codeObject.co_argcount : len(codeObject.co_varnames) + + (-1 if has_varnames else 0) + + (-1 if has_varargs else 0) ] - for name in codeObject.co_varnames[startIndex : startIndex + len(arguments)]: + + # Implements: You can't have a parameter in both args and keywords, + # reporting an easily debugged message + # Implements: You can't have a posonly arg in named (as a side effect of the above) + for name in codeObject.co_varnames[ + 0 : min((len(arguments), codeObject.co_argcount)) + ]: if name in named: raise TypeError( """Argument %r specified both positionally and as a keyword""" """ for calling %r""" % (name, receiver) ) - if not (codeObject.co_flags & 8): + # Implements: You can only passed keyword parameters if the parameter exists and is + # not a positional-only parameter or a varargs or varkeywords parameter. + # Note that this silently drops TypeErrors for passing the name of the varargs or + # varkeyword variables because it only allows through the valid arg-names for + # the function + if not has_varnames: + acceptable = ( + posnamed_arguments[len(arguments) - posonly_count :] + named_onlyarguments + ) # fc does not have a **kwds type parameter, therefore # remove unacceptable arguments. - for arg in list(named): - if arg not in acceptable: - del named[arg] + named = dict([(k, v) for k, v in named.items() if k in acceptable]) return receiver(*arguments, **named) diff --git a/python/grass/pydispatch/saferef.py b/python/grass/pydispatch/saferef.py index 43be1175b19..bf67240d6dd 100644 --- a/python/grass/pydispatch/saferef.py +++ b/python/grass/pydispatch/saferef.py @@ -4,8 +4,12 @@ import traceback import sys -im_func = "__func__" -im_self = "__self__" +if sys.hexversion >= 0x3000000: + im_func = "__func__" + im_self = "__self__" +else: + im_func = "im_func" + im_self = "im_self" def safeRef(target, onDelete=None): @@ -126,9 +130,8 @@ def remove(weak, self=self): traceback.print_exc() except AttributeError: print( - """Exception during saferef %s cleanup """ - """function %s: %s""" % (self, function, e), - file=sys.stderr, + """Exception during saferef %s cleanup function %s: %s""" + % (self, function, e) ) self.deletionMethods = [onDelete] @@ -162,6 +165,8 @@ def __nonzero__(self): """Whether we are still a valid reference""" return self() is not None + __bool__ = __nonzero__ + def __cmp__(self, other): """Compare with another reference""" if not isinstance(other, self.__class__): diff --git a/python/grass/pygrass/modules/grid/grid.py b/python/grass/pygrass/modules/grid/grid.py index 1163a8f3b6d..fb338ca973a 100644 --- a/python/grass/pygrass/modules/grid/grid.py +++ b/python/grass/pygrass/modules/grid/grid.py @@ -42,7 +42,7 @@ def select(parms, ptype): """ for k in parms: par = parms[k] - if par.type == ptype or par.typedesc == ptype and par.value: + if par.type == ptype or (par.typedesc == ptype and par.value): if par.multiple: yield from par.value else: @@ -346,8 +346,8 @@ def get_cmd(cmdd): if isinstance(vals, list) ) ) - cmd.extend(("-%s" % (flg) for flg in cmdd["flags"] if len(flg) == 1)) - cmd.extend(("--%s" % (flg[0]) for flg in cmdd["flags"] if len(flg) > 1)) + cmd.extend(f"-{flg}" for flg in cmdd["flags"] if len(flg) == 1) + cmd.extend(f"--{flg[0]}" for flg in cmdd["flags"] if len(flg) > 1) return cmd @@ -628,7 +628,7 @@ def define_mapset_inputs(self): if inm.type in {"raster", "vector"} and inm.value: if "@" not in inm.value: mset = get_mapset_raster(inm.value) - inm.value = inm.value + "@%s" % mset + inm.value += "@%s" % mset def run(self, patch=True, clean=True): """Run the GRASS command diff --git a/python/grass/pygrass/modules/interface/module.py b/python/grass/pygrass/modules/interface/module.py index b9c298b4a6f..c8a1e723a05 100644 --- a/python/grass/pygrass/modules/interface/module.py +++ b/python/grass/pygrass/modules/interface/module.py @@ -334,11 +334,11 @@ class Module: >>> region.flags.u = True >>> region.flags["3"].value = True # set numeric flags >>> region.get_bash() - 'g.region -p -3 -u' + 'g.region format=plain -p -3 -u' >>> new_region = copy.deepcopy(region) >>> new_region.inputs.res = "10" >>> new_region.get_bash() - 'g.region res=10 -p -3 -u' + 'g.region res=10 format=plain -p -3 -u' >>> neighbors = Module("r.neighbors") >>> neighbors.inputs.input = "mapA" @@ -952,7 +952,8 @@ class MultiModule: ... set_temp_region=True, ... ) >>> str(mm) - 'g.region -p ; g.region -p ; g.region -p ; g.region -p ; g.region -p' + 'g.region format=plain -p ; g.region format=plain -p ; g.region format=plain -p ; \ +g.region format=plain -p ; g.region format=plain -p' >>> t = mm.run() >>> isinstance(t, Process) True diff --git a/python/grass/pygrass/raster/buffer.py b/python/grass/pygrass/raster/buffer.py index 1a2325e2f4c..51420f16610 100644 --- a/python/grass/pygrass/raster/buffer.py +++ b/python/grass/pygrass/raster/buffer.py @@ -4,11 +4,11 @@ _CELL = ("int", "intp", "int8", "int16", "int32", "int64") -CELL = tuple([getattr(np, attr) for attr in _CELL if hasattr(np, attr)]) +CELL = tuple(getattr(np, attr) for attr in _CELL if hasattr(np, attr)) _FCELL = "float", "float16", "float32" -FCELL = tuple([getattr(np, attr) for attr in _FCELL if hasattr(np, attr)]) +FCELL = tuple(getattr(np, attr) for attr in _FCELL if hasattr(np, attr)) _DCELL = "float64", "float128" -DCELL = tuple([getattr(np, attr) for attr in _DCELL if hasattr(np, attr)]) +DCELL = tuple(getattr(np, attr) for attr in _DCELL if hasattr(np, attr)) class Buffer(np.ndarray): diff --git a/python/grass/pygrass/raster/category.py b/python/grass/pygrass/raster/category.py index 31ef5f3ed76..0a5e42093ae 100644 --- a/python/grass/pygrass/raster/category.py +++ b/python/grass/pygrass/raster/category.py @@ -97,7 +97,7 @@ def __dict__(self): diz = {} for cat in self.__iter__(): label, min_cat, max_cat = cat - diz[(min_cat, max_cat)] = label + diz[min_cat, max_cat] = label return diz def __repr__(self): diff --git a/python/grass/pygrass/raster/history.py b/python/grass/pygrass/raster/history.py index 5041c9e55d8..5ec855cf1bb 100644 --- a/python/grass/pygrass/raster/history.py +++ b/python/grass/pygrass/raster/history.py @@ -57,7 +57,6 @@ def __repr__(self): def __del__(self): """Rast_free_history""" - pass def __eq__(self, hist): for attr in self.attrs: diff --git a/python/grass/pygrass/raster/rowio.py b/python/grass/pygrass/raster/rowio.py index 377843642a1..ed43c9a8452 100644 --- a/python/grass/pygrass/raster/rowio.py +++ b/python/grass/pygrass/raster/rowio.py @@ -13,9 +13,7 @@ from grass.pygrass.raster.raster_type import TYPE as RTYPE -CMPFUNC = ctypes.CFUNCTYPE( - ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_int -) +CMPFUNC = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_int) def getmaprow_CELL(fd, buf, row): diff --git a/python/grass/pygrass/raster/testsuite/test_raster_img.py b/python/grass/pygrass/raster/testsuite/test_raster_img.py index c7885c8fcb8..bbc1cb2dabd 100644 --- a/python/grass/pygrass/raster/testsuite/test_raster_img.py +++ b/python/grass/pygrass/raster/testsuite/test_raster_img.py @@ -49,7 +49,7 @@ def test_resampling_to_QImg_1(self): region.adjust() tmpfile = tempfile(False) - tmpfile = tmpfile + ".png" + tmpfile += ".png" a = raster2numpy_img(self.name, region) @@ -67,7 +67,7 @@ def test_resampling_to_QImg_2(self): region.adjust() tmpfile = tempfile(False) - tmpfile = tmpfile + ".png" + tmpfile += ".png" # With array as argument array = np.ndarray((region.rows * region.cols * 4), np.uint8) @@ -88,7 +88,7 @@ def test_resampling_to_QImg_large(self): region.adjust() tmpfile = tempfile(False) - tmpfile = tmpfile + ".png" + tmpfile += ".png" # With array as argument array = np.ndarray((region.rows * region.cols * 4), np.uint8) @@ -109,7 +109,7 @@ def test_resampling_to_QImg_3(self): region.adjust() tmpfile = tempfile(False) - tmpfile = tmpfile + ".png" + tmpfile += ".png" # With array as argument array = np.ndarray((region.rows * region.cols * 4), np.uint8) @@ -130,7 +130,7 @@ def test_resampling_to_QImg_4(self): region.adjust() tmpfile = tempfile(False) - tmpfile = tmpfile + ".png" + tmpfile += ".png" array = raster2numpy_img(rastname=self.name, region=region, color="RGB") diff --git a/python/grass/pygrass/vector/basic.py b/python/grass/pygrass/vector/basic.py index 73eb5bb5557..1ce2dccedd8 100644 --- a/python/grass/pygrass/vector/basic.py +++ b/python/grass/pygrass/vector/basic.py @@ -461,8 +461,7 @@ def delete(self, cat=None, layer=1): """ if cat: self.n_del = libvect.Vect_field_cat_del(self.c_cats, layer, cat) - err_msg = "Layer(%d)/category(%d) number does not exist" - err_msg = err_msg % (layer, cat) + err_msg = "Layer(%d)/category(%d) number does not exist" % (layer, cat) else: self.n_del = libvect.Vect_cat_del(self.c_cats, layer) err_msg = "Layer: %r does not exist" % layer diff --git a/python/grass/pygrass/vector/geometry.py b/python/grass/pygrass/vector/geometry.py index 591ebf38978..bc6ad32f10a 100644 --- a/python/grass/pygrass/vector/geometry.py +++ b/python/grass/pygrass/vector/geometry.py @@ -79,7 +79,6 @@ def read_WKT(string): def read_WKB(buff): """Read the binary buffer and return a geometry object""" - pass def intersects(lineA, lineB, with_z=False): @@ -940,7 +939,6 @@ def first_cat(self): """ # TODO: add this method. # libvect.Vect_get_line_cat(self.c_mapinfo, self.id, self.field) - pass def pop(self, indx): """Return the point in the index position and remove from the Line. @@ -1139,7 +1137,7 @@ def from_wkt(self, wkt): if match: self.reset() for coord in match.groups()[0].strip().split(","): - self.append(tuple([float(e) for e in coord.split(" ")])) + self.append(tuple(float(e) for e in coord.split(" "))) else: return None @@ -1807,7 +1805,6 @@ def get_first_cat(self): ..warning: Not implemented """ - pass @mapinfo_must_be_set def contains_point(self, point, bbox=None): diff --git a/python/grass/pygrass/vector/testsuite/test_geometry.py b/python/grass/pygrass/vector/testsuite/test_geometry.py index c3ad5b6cd63..8680251d8c6 100644 --- a/python/grass/pygrass/vector/testsuite/test_geometry.py +++ b/python/grass/pygrass/vector/testsuite/test_geometry.py @@ -98,7 +98,6 @@ def test_repr(self): def test_buffer(self): """Test buffer method""" # TODO: verify if the buffer depends from the mapset's projection - pass class LineTestCase(TestCase): diff --git a/python/grass/pygrass/vector/testsuite/test_vector.py b/python/grass/pygrass/vector/testsuite/test_vector.py index a57856eca45..5d16333c31f 100644 --- a/python/grass/pygrass/vector/testsuite/test_vector.py +++ b/python/grass/pygrass/vector/testsuite/test_vector.py @@ -37,9 +37,9 @@ def test_getitem_slice(self): """Test that getitem handle correctly the slice starting from 1""" vcoords = ((10.0, 6.0), (12.0, 6.0)) with VectorTopo(self.tmpname, mode="r") as vect: - coords = tuple([pnt.coords() for pnt in vect[:3]]) + coords = tuple(pnt.coords() for pnt in vect[:3]) self.assertTupleEqual(vcoords, coords) - coords = tuple([pnt.coords() for pnt in vect[1:3]]) + coords = tuple(pnt.coords() for pnt in vect[1:3]) self.assertTupleEqual(vcoords, coords) self.vect.close() diff --git a/python/grass/script/core.py b/python/grass/script/core.py index d46c35d3352..a5e14aa92db 100644 --- a/python/grass/script/core.py +++ b/python/grass/script/core.py @@ -178,7 +178,7 @@ def scan(gisbase, directory): gui_path = os.path.join(gisbase, "etc", "gui", "scripts") if os.path.exists(gui_path): os.environ["PATH"] = os.getenv("PATH") + os.pathsep + gui_path - cmd = cmd + os.listdir(gui_path) + cmd += os.listdir(gui_path) return set(cmd), scripts diff --git a/python/grass/temporal/abstract_map_dataset.py b/python/grass/temporal/abstract_map_dataset.py index aa2472a9ee9..f58bcb82d66 100644 --- a/python/grass/temporal/abstract_map_dataset.py +++ b/python/grass/temporal/abstract_map_dataset.py @@ -1240,7 +1240,6 @@ def read_semantic_label_from_grass(self): Currently only implemented in RasterDataset. Otherwise silently pass. """ - pass def set_semantic_label(self, semantic_label): """Set semantic label identifier diff --git a/python/grass/temporal/abstract_space_time_dataset.py b/python/grass/temporal/abstract_space_time_dataset.py index 890061ee64d..929f74e4421 100644 --- a/python/grass/temporal/abstract_space_time_dataset.py +++ b/python/grass/temporal/abstract_space_time_dataset.py @@ -1134,7 +1134,7 @@ def get_registered_maps_as_objects_by_granularity(self, gran=None, dbif=None): if self.is_time_absolute(): end = increment_datetime_by_string(end, gran) else: - end = end + gran + end += gran maplist = AbstractSpaceTimeDataset.resample_maplist_by_granularity( maps, start, end, gran @@ -1997,9 +1997,9 @@ def shift_map_list(maps, gran): end = increment_datetime_by_string(end, gran) map.set_absolute_time(start, end) elif map.is_time_relative(): - start = start + int(gran) + start += int(gran) if end is not None: - end = end + int(gran) + end += int(gran) map.set_relative_time(start, end, map.get_relative_time_unit()) return maps @@ -2048,9 +2048,9 @@ def shift(self, gran, dbif=None): if end is not None: end = increment_datetime_by_string(end, gran) elif self.is_time_relative(): - start = start + int(gran) + start += int(gran) if end is not None: - end = end + int(gran) + end += int(gran) date_list.append((start, end)) diff --git a/python/grass/temporal/core.py b/python/grass/temporal/core.py index 0df95106e0f..60479cf4e08 100644 --- a/python/grass/temporal/core.py +++ b/python/grass/temporal/core.py @@ -479,7 +479,6 @@ def get_available_temporal_mapsets(): tgis_mapsets = {} for mapset in mapsets: - mapset = mapset driver = c_library_interface.get_driver_name(mapset) database = c_library_interface.get_database_name(mapset) diff --git a/python/grass/temporal/datetime_math.py b/python/grass/temporal/datetime_math.py index 3d4b2869854..980c773bf87 100644 --- a/python/grass/temporal/datetime_math.py +++ b/python/grass/temporal/datetime_math.py @@ -483,9 +483,9 @@ def adjust_datetime_to_granularity(mydate, granularity): minutes = 0 hours = 0 if days > weekday: - days = days - weekday # this needs to be fixed + days -= weekday # this needs to be fixed else: - days = days + weekday # this needs to be fixed + days += weekday # this needs to be fixed elif has_months: # Start at the first day of the month at 00:00:00 seconds = 0 minutes = 0 @@ -660,7 +660,7 @@ def compute_datetime_delta(start, end): elif start.day == 1 and end.day == 1: d = end.month - start.month if d < 0: - d = d + 12 * comp["year"] + d += 12 * comp["year"] elif d == 0: d = 12 * comp["year"] comp["month"] = d @@ -678,9 +678,9 @@ def compute_datetime_delta(start, end): else: d = end.hour - start.hour if d < 0: - d = d + 24 + 24 * day_diff + d += 24 + 24 * day_diff else: - d = d + 24 * day_diff + d += 24 * day_diff comp["hour"] = d # Minutes @@ -690,9 +690,9 @@ def compute_datetime_delta(start, end): d = end.minute - start.minute if d != 0: if comp["hour"]: - d = d + 60 * comp["hour"] + d += 60 * comp["hour"] else: - d = d + 24 * 60 * day_diff + d += 24 * 60 * day_diff elif d == 0: if comp["hour"]: d = 60 * comp["hour"] @@ -708,11 +708,11 @@ def compute_datetime_delta(start, end): d = end.second - start.second if d != 0: if comp["minute"]: - d = d + 60 * comp["minute"] + d += 60 * comp["minute"] elif comp["hour"]: - d = d + 3600 * comp["hour"] + d += 3600 * comp["hour"] else: - d = d + 24 * 60 * 60 * day_diff + d += 24 * 60 * 60 * day_diff elif d == 0: if comp["minute"]: d = 60 * comp["minute"] diff --git a/python/grass/temporal/register.py b/python/grass/temporal/register.py index 8133eca734f..0cd339946b9 100644 --- a/python/grass/temporal/register.py +++ b/python/grass/temporal/register.py @@ -557,7 +557,7 @@ def assign_valid_time_to_map( end_time = int(end) if increment: - start_time = start_time + mult * int(increment) + start_time += mult * int(increment) if interval: end_time = start_time + int(increment) diff --git a/python/grass/temporal/temporal_algebra.py b/python/grass/temporal/temporal_algebra.py index c2546ec0d9e..4d48b7119b0 100644 --- a/python/grass/temporal/temporal_algebra.py +++ b/python/grass/temporal/temporal_algebra.py @@ -1193,10 +1193,10 @@ def remove_maps(self): for map in self.removable_maps.values(): map_names[map.get_type()].append(map.get_name()) - for key in map_names.keys(): - if map_names[key]: + for key, value in map_names.items(): + if value: self.msgr.message(_("Removing un-needed or empty %s maps" % (key))) - self._remove_maps(map_names[key], key) + self._remove_maps(value, key) def _remove_maps(self, namelist, map_type): """Remove maps of specific type @@ -1731,7 +1731,7 @@ def compare_bool_value( if count > 0: condition_value_list.append(aggregate) condition_value_list.append(boolean) - count = count + 1 + count += 1 if self.debug: print( diff --git a/python/grass/temporal/temporal_granularity.py b/python/grass/temporal/temporal_granularity.py index 29836959cb8..3022c27ea77 100644 --- a/python/grass/temporal/temporal_granularity.py +++ b/python/grass/temporal/temporal_granularity.py @@ -466,7 +466,7 @@ def compute_absolute_time_granularity(maps): # start time is required in TGIS and expected to be present if end: map_datetime_delta = compute_datetime_delta(start, end) - for time_unit in granularity_units: + for time_unit in granularity_units.keys(): # noqa: PLC0206 if ( time_unit in map_datetime_delta and map_datetime_delta[time_unit] > 0 @@ -484,7 +484,7 @@ def compute_absolute_time_granularity(maps): else: gap_datetime_delta = compute_datetime_delta(previous_start, start) # Add to the set of the smallest granularity in the granularity_units dict - for time_unit in granularity_units: + for time_unit in granularity_units.keys(): # noqa: PLC0206 if ( time_unit in gap_datetime_delta and gap_datetime_delta[time_unit] > 0 @@ -1251,7 +1251,7 @@ def _return(output, tounit, shell): return _return(output, tounit, shell) if k == myunit: num, myunit = v.split(" ") - output = output * ast.literal_eval(num) + output *= ast.literal_eval(num) if tounit == "second" and myunit == tounit: return _return(output, tounit, shell) print(_("Probably you need to invert 'from_gran' and 'to_gran'")) diff --git a/python/grass/temporal/temporal_operator.py b/python/grass/temporal/temporal_operator.py index 572e1559611..7d91ed0f65c 100644 --- a/python/grass/temporal/temporal_operator.py +++ b/python/grass/temporal/temporal_operator.py @@ -199,7 +199,7 @@ class TemporalOperatorLexer: ) # Build the token list - tokens = tokens + tuple(relations.values()) + tokens += tuple(relations.values()) # Regular expression rules for simple tokens t_T_SELECT = r":" @@ -647,7 +647,7 @@ def p_relationlist(self, t): rel_list = [] rel_list.append(t[1]) if isinstance(t[3], list): - rel_list = rel_list + t[3] + rel_list += t[3] else: rel_list.append(t[3]) t[0] = rel_list diff --git a/python/grass/temporal/temporal_raster_base_algebra.py b/python/grass/temporal/temporal_raster_base_algebra.py index 02e28e0e435..8333fbb698e 100644 --- a/python/grass/temporal/temporal_raster_base_algebra.py +++ b/python/grass/temporal/temporal_raster_base_algebra.py @@ -478,7 +478,7 @@ def compare_cmd_value( if count > 0: cmd_value_list.append(aggregate + aggregate) cmd_value_list.append(relationmap.cmd_list) - count = count + 1 + count += 1 if self.debug: print( "compare_cmd_value", diff --git a/python/grass/temporal/temporal_vector_algebra.py b/python/grass/temporal/temporal_vector_algebra.py index d8e8474f26e..4a005a7baeb 100644 --- a/python/grass/temporal/temporal_vector_algebra.py +++ b/python/grass/temporal/temporal_vector_algebra.py @@ -314,14 +314,14 @@ def overlay_cmd_value(self, map_i, tbrelations, function, topolist=["EQUAL"]): mapainput = map_i.get_id() # Append command list of given map to result command list. if "cmd_list" in dir(map_i): - resultlist = resultlist + map_i.cmd_list + resultlist += map_i.cmd_list for topo in topolist: if topo.upper() in tbrelations.keys(): relationmaplist = tbrelations[topo.upper()] for relationmap in relationmaplist: # Append command list of given map to result command list. if "cmd_list" in dir(relationmap): - resultlist = resultlist + relationmap.cmd_list + resultlist += relationmap.cmd_list # Generate an intermediate name name = self.generate_map_name() # Put it into the removalbe map list diff --git a/python/grass/temporal/testsuite/unittests_temporal_raster_algebra_spatial_topology.py b/python/grass/temporal/testsuite/unittests_temporal_raster_algebra_spatial_topology.py index fc3bb6dfa75..481eae5d59d 100644 --- a/python/grass/temporal/testsuite/unittests_temporal_raster_algebra_spatial_topology.py +++ b/python/grass/temporal/testsuite/unittests_temporal_raster_algebra_spatial_topology.py @@ -91,7 +91,6 @@ def setUpClass(cls): def tearDown(self): self.runModule("t.remove", flags="rf", inputs="R", quiet=True) - pass @classmethod def tearDownClass(cls): diff --git a/raster/r.horizon/Makefile b/raster/r.horizon/Makefile index 4407e3c4073..08d3e8db030 100644 --- a/raster/r.horizon/Makefile +++ b/raster/r.horizon/Makefile @@ -2,9 +2,10 @@ MODULE_TOPDIR = ../.. PGM = r.horizon -LIBES = $(PARSONLIB) $(GPROJLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PROJLIB) +LIBES = $(PARSONLIB) $(GPROJLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PROJLIB) $(OPENMP_LIBPATH) $(OPENMP_LIB) DEPENDENCIES = $(GPROJDEP) $(RASTERDEP) $(GISDEP) -EXTRA_INC = $(PROJINC) $(GDALCFLAGS) +EXTRA_CFLAGS = $(OPENMP_CFLAGS) +EXTRA_INC = $(PROJINC) $(GDALCFLAGS) $(OPENMP_INCPATH) include $(MODULE_TOPDIR)/include/Make/Module.make diff --git a/raster/r.horizon/benchmark/benchmark_rhorizon.py b/raster/r.horizon/benchmark/benchmark_rhorizon.py new file mode 100644 index 00000000000..99ec0184aeb --- /dev/null +++ b/raster/r.horizon/benchmark/benchmark_rhorizon.py @@ -0,0 +1,67 @@ +"""Benchmarking of r.horizon +point mode, one direction + +@author Chung-Yuan Liang, 2024 +""" + +from grass.exceptions import CalledModuleError +from grass.pygrass.modules import Module + +import grass.benchmark as bm + + +def main(): + results = [] + metrics = ["time", "speedup", "efficiency"] + mapsizes = [1e6, 2e6, 4e6, 8e6] + + # run benchmarks + for mapsize in mapsizes: + benchmark( + size=int(mapsize**0.5), + step=0, + label=f"r.horizon_{int(mapsize/1e6)}M", + results=results, + ) + + # plot results + for metric in metrics: + bm.nprocs_plot( + results, + title=f"r.horizon raster mode {metric}", + metric=metric, + ) + + +def benchmark(size, step, label, results): + reference = "benchmark_r_horizon_reference_map" + output = "benchmark_r_horizon" + + generate_map(rows=size, cols=size, fname=reference) + module = Module( + "r.horizon", + elevation=reference, + output=output, + direction=0, + step=step, + ) + + results.append(bm.benchmark_nprocs(module, label=label, max_nprocs=8, repeat=3)) + Module( + "g.remove", quiet=True, flags="f", type="raster", pattern="benchmark_r_horizon*" + ) + + +def generate_map(rows, cols, fname): + Module("g.region", flags="p", rows=rows, cols=cols, res=1) + # Generate using r.random.surface if r.surf.fractal fails + try: + print("Generating reference map using r.surf.fractal...") + Module("r.surf.fractal", output=fname, overwrite=True) + except CalledModuleError: + print("r.surf.fractal fails, using r.random.surface instead...") + Module("r.random.surface", output=fname, overwrite=True) + + +if __name__ == "__main__": + main() diff --git a/raster/r.horizon/main.c b/raster/r.horizon/main.c index ff0b2429456..fdec073a76e 100644 --- a/raster/r.horizon/main.c +++ b/raster/r.horizon/main.c @@ -31,6 +31,9 @@ Program was refactored by Anna Petrasova to remove most global variables. * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#if defined(_OPENMP) +#include +#endif #include #include @@ -163,7 +166,7 @@ int main(int argc, char *argv[]) struct { struct Option *elevin, *dist, *coord, *direction, *horizon, *step, *start, *end, *bufferzone, *e_buff, *w_buff, *n_buff, *s_buff, - *maxdistance, *format, *output; + *maxdistance, *format, *output, *nprocs; } parm; struct { @@ -175,6 +178,7 @@ int main(int argc, char *argv[]) G_add_keyword(_("raster")); G_add_keyword(_("solar")); G_add_keyword(_("sun position")); + G_add_keyword(_("parallel")); module->label = _("Computes horizon angle height from a digital elevation model."); module->description = @@ -309,6 +313,8 @@ int main(int argc, char *argv[]) _("Name of file for output (use output=- for stdout)"); parm.output->guisection = _("Point mode"); + parm.nprocs = G_define_standard_option(G_OPT_M_NPROCS); + flag.horizonDistance = G_define_flag(); flag.horizonDistance->key = 'l'; flag.horizonDistance->description = @@ -328,6 +334,8 @@ int main(int argc, char *argv[]) if (G_parser(argc, argv)) exit(EXIT_FAILURE); + G_set_omp_num_threads(parm.nprocs); + struct Cell_head cellhd; struct Cell_head new_cellhd; G_get_set_window(&cellhd); @@ -857,6 +865,7 @@ void calculate_point_mode(const Settings *settings, const Geometry *geometry, horizons = json_value_get_array(horizons_value); break; } + for (int i = 0; i < printCount; i++) { JSON_Value *value; JSON_Object *object; @@ -1179,6 +1188,7 @@ void calculate_raster_mode(const Settings *settings, const Geometry *geometry, _("Calculating map %01d of %01d (angle %.2f, raster map <%s>)"), (k + 1), arrayNumInt, angle_deg, shad_filename); +#pragma omp parallel for schedule(static, 1) default(shared) for (int j = hor_row_start; j < hor_row_end; j++) { G_percent(j - hor_row_start, hor_numrows - 1, 2); for (int i = hor_col_start; i < hor_col_end; i++) { @@ -1219,12 +1229,11 @@ void calculate_raster_mode(const Settings *settings, const Geometry *geometry, if (settings->degreeOutput) { shadow_angle *= rad2deg; } - horizon_raster[j - buffer_s][i - buffer_w] = shadow_angle; } /* undefs */ - } - } + } /* end of loop over columns */ + } /* end of parallel section */ G_debug(1, "OUTGR() starts..."); OUTGR(settings, shad_filename, cellhd); @@ -1265,4 +1274,9 @@ void calculate_raster_mode(const Settings *settings, const Geometry *geometry, Rast_write_history(shad_filename, &history); G_free(shad_filename); } + + /* free memory */ + for (int l = 0; l < hor_numrows; l++) + G_free(horizon_raster[l]); + G_free(horizon_raster); } diff --git a/raster/r.horizon/r.horizon.html b/raster/r.horizon/r.horizon.html index 5770fd248dd..513af64b8c4 100644 --- a/raster/r.horizon/r.horizon.html +++ b/raster/r.horizon/r.horizon.html @@ -121,7 +121,9 @@

Input parameters:

raster coordinates (e.g., for latitude-longitude buffers are measured in degrees). -

METHOD

+

NOTES

+ +

Method

The calculation method is based on the method used in r.sun to calculate shadows. It starts at a very shallow angle and walks @@ -145,6 +147,34 @@

METHOD

All horizon values are positive (or zero). While negative values are in theory possible, r.horizon currently does not support them. + +

Performance

+Parallel processing is implemented for the raster mode. +To enable parallel processing, the user can specify the number of threads +to be used with the nprocs parameter. +Figures below show benchmark results running on +Intel® Core™ i5-13600K CPU @ 3.5GHz. +See benchmark scripts +in the source code for more details. + +

+The iterations of the algorithm used in r.horizon +depends on the topography. As a result, the benchmark results may vary +depending on the topography of the study area. + +

+ time for r.horizon with different map sizes + speedup for r.horizon with different map sizes + efficiency for r.horizon with different map sizes +
+ Figure: Benchmark shows execution time, parallel speedup and + efficiency for different numbers of cells (1M, 2M, 4M, and 8M). +
+ +

EXAMPLES

The examples are intended for the North Carolina sample dataset. diff --git a/raster/r.horizon/rhorizon_raster_efficiency.png b/raster/r.horizon/rhorizon_raster_efficiency.png new file mode 100644 index 00000000000..7b37d1fff46 Binary files /dev/null and b/raster/r.horizon/rhorizon_raster_efficiency.png differ diff --git a/raster/r.horizon/rhorizon_raster_speedup.png b/raster/r.horizon/rhorizon_raster_speedup.png new file mode 100644 index 00000000000..7609a75c163 Binary files /dev/null and b/raster/r.horizon/rhorizon_raster_speedup.png differ diff --git a/raster/r.horizon/rhorizon_raster_time.png b/raster/r.horizon/rhorizon_raster_time.png new file mode 100644 index 00000000000..d02f8f4450d Binary files /dev/null and b/raster/r.horizon/rhorizon_raster_time.png differ diff --git a/raster/r.horizon/testsuite/test_r_horizon_parallel.py b/raster/r.horizon/testsuite/test_r_horizon_parallel.py new file mode 100644 index 00000000000..605f94365b2 --- /dev/null +++ b/raster/r.horizon/testsuite/test_r_horizon_parallel.py @@ -0,0 +1,528 @@ +""" +TEST: test_r_horizon.py + +AUTHOR(S): Anna Petrasova + +PURPOSE: Test r.horizon + +COPYRIGHT: (C) 2015-2024 Anna Petrasova + + This program is free software under the GNU General Public + License (>=v2). Read the file COPYING that comes with GRASS + for details. +""" + +import json + +from grass.gunittest.case import TestCase +from grass.gunittest.main import test +from grass.gunittest.gmodules import SimpleModule +from grass.script import raster_what + + +ref1 = """azimuth,horizon_height +180.000000,0.023101 +""" + +ref2 = """azimuth,horizon_height +180.000000,0.023101 +200.000000,0.034850 +220.000000,0.050549 +240.000000,0.048211 +260.000000,0.053101 +280.000000,0.039774 +300.000000,0.032360 +320.000000,0.014804 +340.000000,0.000000 +360.000000,0.004724 +20.000000,0.012612 +40.000000,0.015207 +60.000000,0.014344 +80.000000,0.011044 +100.000000,0.012192 +120.000000,0.007462 +140.000000,0.004071 +160.000000,0.015356 +""" + +ref3 = """azimuth,horizon_height +180.000000,0.023101 +200.000000,0.034850 +220.000000,0.050549 +240.000000,0.048211 +260.000000,0.053101 +280.000000,0.039774 +300.000000,0.032360 +320.000000,0.014804 +340.000000,0.000000 +360.000000,0.004724 +20.000000,0.012612 +40.000000,0.015207 +60.000000,0.014344 +80.000000,0.011044 +100.000000,0.012192 +120.000000,0.007462 +140.000000,0.004071 +160.000000,0.015356 +""" + +ref4 = """azimuth,horizon_height +0.000000,0.197017 +20.000000,0.196832 +40.000000,0.196875 +60.000000,0.196689 +80.000000,0.196847 +100.000000,0.196645 +120.000000,0.196969 +140.000000,0.196778 +160.000000,0.196863 +180.000000,0.197017 +200.000000,0.196832 +220.000000,0.196875 +240.000000,0.196689 +260.000000,0.196847 +280.000000,0.196645 +300.000000,0.196969 +320.000000,0.196778 +340.000000,0.196863 +""" + +ref5 = """azimuth,horizon_height,horizon_distance +0.000000,0.197017,5000.040000 +20.000000,0.196832,5004.837660 +40.000000,0.196875,5003.728610 +60.000000,0.196689,5008.552685 +80.000000,0.196847,5004.448022 +100.000000,0.196645,5009.690609 +120.000000,0.196969,5001.279836 +140.000000,0.196778,5006.246099 +160.000000,0.196863,5004.018385 +180.000000,0.197017,5000.040000 +200.000000,0.196832,5004.837660 +220.000000,0.196875,5003.728610 +240.000000,0.196689,5008.552685 +260.000000,0.196847,5004.448022 +280.000000,0.196645,5009.690609 +300.000000,0.196969,5001.279836 +320.000000,0.196778,5006.246099 +340.000000,0.196863,5004.018385 +""" + +ref6 = """azimuth,horizon_height,horizon_distance +180.000000,0.023101,420.000000 +200.000000,0.034850,436.577599 +220.000000,0.050549,184.390889 +240.000000,0.048211,197.230829 +260.000000,0.053101,162.788206 +280.000000,0.039774,253.179778 +300.000000,0.032360,277.848880 +320.000000,0.014804,262.488095 +340.000000,0.000000,0.000000 +360.000000,0.004724,2780.017986 +20.000000,0.012612,1148.259553 +40.000000,0.015207,1334.166406 +60.000000,0.014344,1867.966809 +80.000000,0.011044,2964.203097 +100.000000,0.012192,1828.223181 +120.000000,0.007462,4270.667395 +140.000000,0.004071,5659.231397 +160.000000,0.015356,1666.883319 +""" + + +class TestHorizon(TestCase): + circle = "circle" + horizon = "test_horizon_from_elevation" + horizon_output = "test_horizon_output_from_elevation" + + @classmethod + def setUpClass(cls): + cls.use_temp_region() + cls.runModule("g.region", raster="elevation") + cls.runModule( + "r.circle", + flags="b", + output=cls.circle, + coordinates=(637505, 221755), + min=5000, + multiplier=1000, + ) + cls.runModule("r.null", map=cls.circle, null=0) + + @classmethod + def tearDownClass(cls): + cls.runModule("g.remove", flags="f", type="raster", name=cls.circle) + cls.del_temp_region() + + def setUp(self): + self.runModule("g.region", raster="elevation") + + def tearDown(self): + """Remove horizon map after each test method""" + self.runModule("g.remove", flags="f", type="raster", name=self.horizon) + self.runModule( + "g.remove", flags="f", type="raster", pattern=self.horizon_output + "*" + ) + + def test_point_mode_one_direction(self): + """Test mode with 1 point and 1 direction""" + module = SimpleModule( + "r.horizon", + elevation="elevation", + coordinates=(634720, 216180), + output=self.horizon, + direction=180, + step=0, + nprocs=4, + ) + self.assertModule(module) + stdout = module.outputs.stdout + self.assertMultiLineEqual(first=ref1, second=stdout) + + def test_point_mode_multiple_direction(self): + """Test mode with 1 point and multiple directions""" + module = SimpleModule( + "r.horizon", + elevation="elevation", + coordinates=(634720, 216180), + output=self.horizon, + direction=180, + step=20, + nprocs=4, + ) + self.assertModule(module) + stdout = module.outputs.stdout + self.assertMultiLineEqual(first=ref2, second=stdout) + + # include nulls along the edge + self.runModule("g.region", raster="elevation", w="w-100") + self.assertModule(module) + stdout = module.outputs.stdout + self.assertMultiLineEqual(first=ref2, second=stdout) + + def test_point_mode_multiple_points_and_directions(self): + """Test mode with 2 identical points and multiple directions""" + module = SimpleModule( + "r.horizon", + elevation="elevation", + coordinates=(634720, 216180, 634720, 216180), + output=self.horizon, + direction=180, + step=20, + nprocs=4, + ) + self.assertModule(module) + stdout = module.outputs.stdout + self.assertMultiLineEqual(first=ref2 + ref2, second=stdout) + + def test_point_mode_multiple_direction_json(self): + """Test mode with 1 point and multiple directions with JSON""" + module = SimpleModule( + "r.horizon", + elevation="elevation", + coordinates=(634720, 216180), + output=self.horizon, + direction=180, + step=20, + nprocs=4, + format="json", + ) + self.assertModule(module) + stdout = json.loads(module.outputs.stdout) + horizons = [] + reference = {} + for line in ref6.splitlines()[1:]: + azimuth, horizon, distance = line.split(",") + horizons.append( + { + "azimuth": float(azimuth), + "angle": float(horizon), + "distance": float(distance), + } + ) + reference["x"] = 634720.0 + reference["y"] = 216180.0 + reference["horizons"] = horizons + + self.assertListEqual([reference], stdout) + + def test_point_mode_multiple_points_and_directions_json(self): + """Test mode with 2 identical points and multiple directions with JSON""" + module = SimpleModule( + "r.horizon", + elevation="elevation", + coordinates=(634720, 216180, 634720, 216180), + output=self.horizon, + direction=180, + step=20, + nprocs=4, + format="json", + ) + self.assertModule(module) + stdout = json.loads(module.outputs.stdout) + horizons = [] + reference = {} + for line in ref6.splitlines()[1:]: + azimuth, horizon, distance = line.split(",") + horizons.append( + { + "azimuth": float(azimuth), + "angle": float(horizon), + "distance": float(distance), + } + ) + reference["x"] = 634720.0 + reference["y"] = 216180.0 + reference["horizons"] = horizons + + self.assertListEqual([reference, reference], stdout) + + def test_point_mode_multiple_direction_artificial(self): + """Test mode with 1 point and multiple directions with artificial surface""" + module = SimpleModule( + "r.horizon", + elevation=self.circle, + coordinates=(637505, 221755), + output=self.horizon, + direction=0, + step=20, + nprocs=4, + ) + self.assertModule(module) + stdout = module.outputs.stdout + self.assertMultiLineEqual(first=ref4, second=stdout) + + def test_point_mode_multiple_direction_artificial_distance(self): + """With 1 point, more directions on artificial surface, distance in output""" + module = SimpleModule( + "r.horizon", + elevation=self.circle, + coordinates=(637505, 221755), + output=self.horizon, + direction=0, + step=20, + nprocs=4, + flags="l", + ) + self.assertModule(module) + stdout = module.outputs.stdout + self.assertMultiLineEqual(first=ref5, second=stdout) + + module = SimpleModule( + "r.horizon", + elevation=self.circle, + coordinates=(637505, 221755), + output=self.horizon, + direction=0, + step=20, + nprocs=4, + flags="l", + format="json", + ) + self.assertModule(module) + stdout = json.loads(module.outputs.stdout) + horizons = [] + reference = {} + for line in ref5.splitlines()[1:]: + azimuth, horizon, distance = line.split(",") + horizons.append( + { + "azimuth": float(azimuth), + "angle": float(horizon), + "distance": float(distance), + } + ) + reference["x"] = 637505.0 + reference["y"] = 221755.0 + reference["horizons"] = horizons + + self.assertListEqual([reference], stdout) + + def test_raster_mode_one_direction(self): + """Test mode with one direction and against point mode""" + module = SimpleModule( + "r.horizon", + elevation="elevation", + output=self.horizon_output, + direction=50, + nprocs=4, + ) + self.assertModule(module) + ref = { + "min": 0, + "max": 0.70678365230560, + "stddev": 0.0360724286360789, + } + output = "test_horizon_output_from_elevation_050" + self.assertRasterFitsUnivar( + raster=output, + reference=ref, + precision=1e6, + ) + + # test if point mode matches raster mode + coordinates = [ + (634725, 216185), + (633315, 217595), + (633555, 223405), + (639955, 220605), + (637505, 219705), + (641105, 222225), + ] + for coordinate in coordinates: + module = SimpleModule( + "r.horizon", + elevation="elevation", + coordinates=coordinate, + output=self.horizon, + direction=50, + step=0, + ) + self.assertModule(module) + stdout = module.outputs.stdout + first = float(stdout.splitlines()[-1].split(",")[-1]) + what = raster_what(output, coord=coordinate) + second = float(what[0][output]["value"]) + self.assertAlmostEqual(first=first, second=second, delta=0.000001) + + def test_raster_mode_multiple_direction(self): + self.runModule("g.region", raster="elevation", res=100) + module = SimpleModule( + "r.horizon", + elevation="elevation", + output=self.horizon_output, + start=10, + end=50, + step=15.512, + nprocs=4, + ) + self.assertModule(module) + module_list = SimpleModule( + "g.list", type="raster", pattern=self.horizon_output + "*" + ) + self.runModule(module_list) + stdout = module_list.outputs.stdout.strip() + self.assertMultiLineEqual( + first=( + "test_horizon_output_from_elevation_010_000\n" + "test_horizon_output_from_elevation_025_512\n" + "test_horizon_output_from_elevation_041_024" + ), + second=stdout, + ) + + def test_raster_mode_multiple_direction_offset(self): + self.runModule("g.region", raster="elevation", res=100) + module = SimpleModule( + "r.horizon", + elevation="elevation", + output=self.horizon_output, + start=10, + end=50, + step=15.512, + nprocs=4, + direction=80, + ) + self.assertModule(module) + module_list = SimpleModule( + "g.list", type="raster", pattern=self.horizon_output + "*" + ) + self.runModule(module_list) + stdout = module_list.outputs.stdout.strip() + self.assertMultiLineEqual( + first=( + "test_horizon_output_from_elevation_090_000\n" + "test_horizon_output_from_elevation_105_512\n" + "test_horizon_output_from_elevation_121_024" + ), + second=stdout, + ) + + def test_raster_mode_bufferzone(self): + """Test buffer 100 m and 109 m with resolution 10 gives the same result""" + self.runModule( + "g.region", + raster="elevation", + n="n-5000", + s="s+5000", + e="e-5000", + w="w+5000", + ) + # raises ValueError from pygrass parameter check + self.assertRaises( + ValueError, + SimpleModule, + "r.horizon", + elevation="elevation", + output=self.horizon_output, + direction=50, + bufferzone=-100, + nprocs=4, + ) + self.assertRaises( + ValueError, + SimpleModule, + "r.horizon", + elevation="elevation", + output=self.horizon_output, + direction=50, + e_buff=100, + n_buff=0, + s_buff=-100, + w_buff=-100, + nprocs=4, + ) + module = SimpleModule( + "r.horizon", + elevation="elevation", + output=self.horizon_output, + direction=50, + bufferzone=100, + nprocs=4, + ) + self.assertModule(module) + ref = { + "mean": 0.0344791, + } + output = "test_horizon_output_from_elevation_050" + self.assertRasterFitsUnivar( + raster=output, + reference=ref, + precision=1e-6, + ) + module = SimpleModule( + "r.horizon", + elevation="elevation", + output=self.horizon_output, + direction=50, + bufferzone=103, + nprocs=4, + ) + self.assertModule(module) + self.assertRasterFitsUnivar( + raster=output, + reference=ref, + precision=1e-6, + ) + module = SimpleModule( + "r.horizon", + elevation="elevation", + output=self.horizon_output, + direction=50, + bufferzone=95, + nprocs=4, + ) + self.assertModule(module) + ref = { + "mean": 0.0344624, + } + self.assertRasterFitsUnivar( + raster=output, + reference=ref, + precision=1e-6, + ) + self.runModule("g.region", raster="elevation") + + +if __name__ == "__main__": + test() diff --git a/raster/r.in.xyz/main.c b/raster/r.in.xyz/main.c index 0257f22a97d..a4c7dfde6ed 100644 --- a/raster/r.in.xyz/main.c +++ b/raster/r.in.xyz/main.c @@ -101,7 +101,7 @@ int main(int argc, char *argv[]) double zrange_min, zrange_max, vrange_min, vrange_max, d_tmp; char *fs; /* field delim */ off_t filesize; - int linesize; + int linesize = 0; unsigned long estimated_lines, line; int from_stdin; int can_seek; @@ -1213,7 +1213,14 @@ int scan_bounds(FILE *fp, int xcol, int ycol, int zcol, int vcol, char *fs, unsigned long line; int first, max_col; char buff[BUFFSIZE]; - double min_x, max_x, min_y, max_y, min_z, max_z, min_v, max_v; + double min_x = 0.0; + double max_x = 0.0; + double min_y = 0.0; + double max_y = 0.0; + double min_z = 0.0; + double max_z = 0.0; + double min_v = 0.0; + double max_v = 0.0; char **tokens; int ntokens; /* number of tokens */ double x, y, z, v; diff --git a/raster/r.out.mpeg/main.c b/raster/r.out.mpeg/main.c index 0ac3b631b80..7782033afec 100644 --- a/raster/r.out.mpeg/main.c +++ b/raster/r.out.mpeg/main.c @@ -95,6 +95,7 @@ int main(int argc, char **argv) struct Flag *conv; int i; int *sdimp, longdim, r_out; + size_t len; G_gisinit(argv[0]); @@ -144,7 +145,10 @@ int main(int argc, char **argv) parse_command(viewopts, vfiles, &numviews, &frames); /* output file */ - strcpy(outfile, out->answer); + len = G_strlcpy(outfile, out->answer, sizeof(outfile)); + if (len >= sizeof(outfile)) { + G_fatal_error(_("Name <%s> is too long"), out->answer); + } r_out = 0; if (conv->answer) @@ -395,9 +399,9 @@ static void mlist(const char *element, const char *wildarg, const char *outfile) if (strcmp(mapset, ".") == 0) mapset = G_mapset(); - sprintf(type_arg, "type=%s", element); - sprintf(pattern_arg, "pattern=%s", wildarg); - sprintf(mapset_arg, "mapset=%s", mapset); + snprintf(type_arg, sizeof(type_arg), "type=%s", element); + snprintf(pattern_arg, sizeof(pattern_arg), "pattern=%s", wildarg); + snprintf(mapset_arg, sizeof(mapset_arg), "mapset=%s", mapset); G_spawn_ex("g.list", "g.list", type_arg, pattern_arg, mapset_arg, SF_REDIRECT_FILE, SF_STDOUT, SF_MODE_APPEND, outfile, NULL); diff --git a/raster/r.out.mpeg/write.c b/raster/r.out.mpeg/write.c index f2152b3e7f6..ccb2c896562 100644 --- a/raster/r.out.mpeg/write.c +++ b/raster/r.out.mpeg/write.c @@ -197,12 +197,16 @@ void write_params(char *mpfilename, char *yfiles[], char *outfile, int frames, FILE *fp; char dir[1000], *enddir; int i, dirlen = 0; + size_t len; if (NULL == (fp = fopen(mpfilename, "w"))) G_fatal_error(_("Unable to create temporary files.")); if (!fly) { - strcpy(dir, yfiles[0]); + len = G_strlcpy(dir, yfiles[0], sizeof(dir)); + if (len >= sizeof(dir)) { + G_fatal_error(_("Directory <%s> too long"), yfiles[0]); + } enddir = strrchr(dir, '/'); if (enddir) { diff --git a/raster/r.path/main.c b/raster/r.path/main.c index e58995695de..5edcdfa1d95 100644 --- a/raster/r.path/main.c +++ b/raster/r.path/main.c @@ -139,6 +139,7 @@ int main(int argc, char **argv) struct line_cats *Cats; struct Map_info vout, *pvout; char *desc = NULL; + size_t len; G_gisinit(argv[0]); @@ -219,13 +220,23 @@ int main(int argc, char **argv) if (G_parser(argc, argv)) exit(EXIT_FAILURE); - strcpy(dir_name, opt.dir->answer); + len = G_strlcpy(dir_name, opt.dir->answer, sizeof(dir_name)); + if (len >= sizeof(dir_name)) { + G_fatal_error(_("Name <%s> is too long"), opt.dir->answer); + } *map_name = '\0'; *out_name = '\0'; if (opt.rast->answer) { - strcpy(out_name, opt.rast->answer); - if (opt.val->answer) - strcpy(map_name, opt.val->answer); + len = G_strlcpy(out_name, opt.rast->answer, sizeof(out_name)); + if (len >= sizeof(out_name)) { + G_fatal_error(_("Name <%s> is too long"), opt.rast->answer); + } + } + if (opt.rast->answer && opt.val->answer) { + len = G_strlcpy(map_name, opt.val->answer, sizeof(map_name)); + if (len >= sizeof(map_name)) { + G_fatal_error(_("Name <%s> is too long"), opt.val->answer); + } } pvout = NULL; diff --git a/raster/r.report/Makefile b/raster/r.report/Makefile index 34d6942690f..0bab2b40c4a 100644 --- a/raster/r.report/Makefile +++ b/raster/r.report/Makefile @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../.. PGM = r.report -LIBES = $(RASTERLIB) $(GISLIB) +LIBES = $(RASTERLIB) $(GISLIB) $(PARSONLIB) DEPENDENCIES = $(RASTERDEP) $(GISDEP) include $(MODULE_TOPDIR)/include/Make/Module.make diff --git a/raster/r.report/format.c b/raster/r.report/format.c index 8319fe747ed..41cc3ca6991 100644 --- a/raster/r.report/format.c +++ b/raster/r.report/format.c @@ -73,3 +73,93 @@ int format_double(double v, char *buf, int n, int dp) return 0; } + +void compute_unit_format(int unit1, int unit2, enum OutputFormat format) +{ + int i, ns, len; + char num[100]; + int need_format; + + /* examine units, determine output format */ + for (i = unit1; i <= unit2; i++) { + if (format == PLAIN) { + need_format = 1; + } + else { + need_format = 0; + } + unit[i].label[0] = ""; + unit[i].label[1] = ""; + + switch (unit[i].type) { + case CELL_COUNTS: + unit[i].label[0] = " cell"; + unit[i].label[1] = "count"; + + if (need_format) { + need_format = 0; + unit[i].len = 5; + ns = 0; + snprintf(num, sizeof(num), "%ld", count_sum(&ns, -1)); + len = strlen(num); + if (len > unit[i].len) + unit[i].len = len; + } + break; + + case PERCENT_COVER: + unit[i].label[0] = " % "; + unit[i].label[1] = "cover"; + + if (need_format) { + need_format = 0; + unit[i].dp = 2; + unit[i].len = 6; + unit[i].eformat = 0; + } + break; + + case SQ_METERS: + unit[i].label[0] = "square"; + unit[i].label[1] = "meters"; + unit[i].factor = 1.0; + break; + + case SQ_KILOMETERS: + unit[i].label[0] = " square "; + unit[i].label[1] = "kilometers"; + unit[i].factor = 1.0e-6; + break; + + case ACRES: + unit[i].label[0] = ""; + unit[i].label[1] = "acres"; + unit[i].factor = 2.47105381467165e-4; /* 640 acres in a sq mile */ + break; + + case HECTARES: + unit[i].label[0] = ""; + unit[i].label[1] = "hectares"; + unit[i].factor = 1.0e-4; + break; + + case SQ_MILES: + unit[i].label[0] = "square"; + unit[i].label[1] = " miles"; + unit[i].factor = 3.86102158542446e-7; /* 1 / ( (0.0254m/in * 12in/ft + * 5280ft/mi)^2 ) */ + break; + + default: + G_fatal_error("Unit %d not yet supported", unit[i].type); + } + if (need_format) { + unit[i].dp = 6; + unit[i].len = 10; + unit[i].eformat = 0; + ns = 0; + format_parms(area_sum(&ns, -1) * unit[i].factor, &unit[i].len, + &unit[i].dp, &(unit[i].eformat), e_format); + } + } +} diff --git a/raster/r.report/global.h b/raster/r.report/global.h index 1b23ab6b813..a05ac1ae1d5 100644 --- a/raster/r.report/global.h +++ b/raster/r.report/global.h @@ -6,6 +6,7 @@ #endif #include +#include #define SORT_DEFAULT 0 #define SORT_ASC 1 @@ -52,6 +53,8 @@ extern int nunits; #define DEFAULT_PAGE_LENGTH "0" #define DEFAULT_PAGE_WIDTH "79" +enum OutputFormat { PLAIN, JSON }; + extern int page_width; extern int page_length; extern int masking; @@ -67,6 +70,7 @@ extern char *stats_file; extern char *no_data_str; extern int stats_flag; extern int nsteps, cat_ranges, as_int; +extern enum OutputFormat format; extern int *is_fp; extern DCELL *DMAX, *DMIN; @@ -81,6 +85,7 @@ extern struct Categories *labels; int format_parms(double, int *, int *, int *, int); int scient_format(double, char *, int, int); int format_double(double, char *, int, int); +void compute_unit_format(int, int, enum OutputFormat); /* header.c */ int header(int, int); @@ -112,6 +117,12 @@ char *construct_cat_label(int, CELL); /* prt_unit.c */ int print_unit(int, int, int); +/* prt_json.c */ +JSON_Value *make_units(int, int); +JSON_Value *make_category(int, int, JSON_Value *); +JSON_Value *make_categories(int, int, int); +void print_json(); + /* report.c */ int report(void); diff --git a/raster/r.report/main.c b/raster/r.report/main.c index bb04db163c2..5b13e23411e 100644 --- a/raster/r.report/main.c +++ b/raster/r.report/main.c @@ -52,6 +52,8 @@ int maskfd; CELL *mask; CELL NULL_CELL; +enum OutputFormat format; + char fs[2]; struct Categories *labels; diff --git a/raster/r.report/parse.c b/raster/r.report/parse.c index 2b18491b8a0..44e7bc5dbd3 100644 --- a/raster/r.report/parse.c +++ b/raster/r.report/parse.c @@ -17,6 +17,7 @@ int parse_command_line(int argc, char *argv[]) struct Option *nv; struct Option *nsteps; struct Option *sort; + struct Option *format; } parms; struct { struct Flag *f; @@ -103,6 +104,9 @@ int parse_command_line(int argc, char *argv[]) _("Sort by cell counts in descending order")); parms.sort->guisection = _("Formatting"); + parms.format = G_define_standard_option(G_OPT_F_FORMAT); + parms.format->guisection = _("Formatting"); + flags.h = G_define_flag(); flags.h->key = 'h'; flags.h->description = _("Suppress page headers"); @@ -172,6 +176,13 @@ int parse_command_line(int argc, char *argv[]) cat_ranges = flags.C->answer; as_int = flags.i->answer; + if (strcmp(parms.format->answer, "json") == 0) { + format = JSON; + } + else { + format = PLAIN; + } + for (i = 0; parms.cell->answers[i]; i++) parse_layer(parms.cell->answers[i]); if (parms.units->answers) diff --git a/raster/r.report/prt_json.c b/raster/r.report/prt_json.c new file mode 100644 index 00000000000..94590ea7b9f --- /dev/null +++ b/raster/r.report/prt_json.c @@ -0,0 +1,205 @@ +#include +#include +#include +#include "global.h" +#include +#include + +JSON_Value *make_units(int ns, int nl) +{ + JSON_Value *units_value = json_value_init_array(); + JSON_Array *units_array = json_array(units_value); + for (int i = 0; i < nunits; i++) { + int _ns = ns; + + JSON_Value *unit_value = json_value_init_object(); + JSON_Object *unit_object = json_object(unit_value); + + if (unit[i].type == CELL_COUNTS) { + json_object_set_string(unit_object, "unit", "cell counts"); + json_object_set_number(unit_object, "value", count_sum(&_ns, nl)); + } + else if (unit[i].type == PERCENT_COVER) { + json_object_set_string(unit_object, "unit", "% cover"); + int k = ns - 1; + while (k >= 0 && same_cats(k, ns, nl - 1)) + k--; + k++; + double area = area_sum(&k, nl - 1); + area = 100.0 * area_sum(&_ns, nl) / area; + json_object_set_number(unit_object, "value", area); + } + else { + char *unit_name = NULL; + if (unit[i].type == ACRES) { + unit_name = "acres"; + } + else if (unit[i].type == HECTARES) { + unit_name = "hectares"; + } + else if (unit[i].type == SQ_MILES) { + unit_name = "square miles"; + } + else if (unit[i].type == SQ_METERS) { + unit_name = "square meters"; + } + else if (unit[i].type == SQ_KILOMETERS) { + unit_name = "square kilometers"; + } + json_object_set_string(unit_object, "unit", unit_name); + json_object_set_number(unit_object, "value", + area_sum(&_ns, nl) * unit[i].factor); + } + json_array_append_value(units_array, unit_value); + } + return units_value; +} + +JSON_Value *make_category(int ns, int nl, JSON_Value *sub_categories) +{ + JSON_Value *object_value = json_value_init_object(); + JSON_Object *object = json_object(object_value); + + CELL *cats = Gstats[ns].cats; + json_object_set_number(object, "category", cats[nl]); + + DCELL dLow, dHigh; + + if (!is_fp[nl] || as_int) + json_object_set_string(object, "label", + Rast_get_c_cat(&cats[nl], &layers[nl].labels)); + else { + /* find or construct the label for floating point range to print */ + if (Rast_is_c_null_value(&cats[nl])) + json_object_set_null(object, "label"); + else if (cat_ranges) { + json_object_set_string(object, "label", + Rast_get_ith_d_cat(&layers[nl].labels, + cats[nl], &dLow, &dHigh)); + } + else { + dLow = (DMAX[nl] - DMIN[nl]) / (double)nsteps * + (double)(cats[nl] - 1) + + DMIN[nl]; + dHigh = (DMAX[nl] - DMIN[nl]) / (double)nsteps * (double)cats[nl] + + DMIN[nl]; + + json_object_set_string(object, "label", "from to"); + + JSON_Value *range_value = json_value_init_object(); + JSON_Object *range_object = json_object(range_value); + json_object_set_number(range_object, "from", dLow); + json_object_set_number(range_object, "to", dHigh); + json_object_set_value(object, "range", range_value); + } + } + + JSON_Value *units_value = make_units(ns, nl); + json_object_set_value(object, "units", units_value); + + if (sub_categories != NULL) { + json_object_set_value(object, "categories", sub_categories); + } + return object_value; +} + +JSON_Value *make_categories(int start, int end, int level) +{ + JSON_Value *array_value = json_value_init_array(); + JSON_Array *array = json_array(array_value); + if (level == nlayers - 1) { + for (int i = start; i < end; i++) { + JSON_Value *category = make_category(i, level, NULL); + json_array_append_value(array, category); + } + } + else { + while (start < end) { + int curr = start; + while ((curr < end) && same_cats(start, curr, level)) { + curr++; + } + JSON_Value *sub_categories = + make_categories(start, curr, level + 1); + JSON_Value *category = make_category(start, level, sub_categories); + json_array_append_value(array, category); + start = curr; + } + } + return array_value; +} + +void print_json() +{ + compute_unit_format(0, nunits - 1, JSON); + + JSON_Value *root_value = json_value_init_object(); + JSON_Object *root_object = json_object(root_value); + + json_object_set_string(root_object, "location", G_location()); + + char date[64]; + time_t now; + struct tm *tm_info; + + time(&now); + tm_info = localtime(&now); + strftime(date, 64, "%Y-%m-%dT%H:%M:%S%z", tm_info); + json_object_set_string(root_object, "created", date); + + JSON_Value *region_value = json_value_init_object(); + JSON_Object *region_object = json_object(region_value); + json_object_set_number(region_object, "north", window.north); + json_object_set_number(region_object, "south", window.south); + json_object_set_number(region_object, "east", window.east); + json_object_set_number(region_object, "west", window.west); + json_object_set_number(region_object, "ew_res", window.ew_res); + json_object_set_number(region_object, "ns_res", window.ns_res); + json_object_set_value(root_object, "region", region_value); + + char *mask = maskinfo(); + if (strcmp(mask, "none") == 0) { + json_object_set_null(root_object, "mask"); + } + else { + json_object_set_string(root_object, "mask", mask); + } + + JSON_Value *maps_value = json_value_init_array(); + JSON_Array *maps_array = json_array(maps_value); + + for (int i = 0; i < nlayers; i++) { + JSON_Value *map_value = json_value_init_object(); + JSON_Object *map_object = json_object(map_value); + json_object_set_string(map_object, "name", layers[i].name); + + char *label; + label = Rast_get_cats_title(&(layers[i].labels)); + if (label == NULL || *label == 0) { + json_object_set_null(map_object, "label"); + } + else { + G_strip(label); + json_object_set_string(map_object, "label", label); + } + + json_object_set_string(map_object, "type", "raster"); + json_array_append_value(maps_array, map_value); + } + json_object_set_value(root_object, "maps", maps_value); + + JSON_Value *root_categories_value = make_categories(0, nstats, 0); + json_object_set_value(root_object, "categories", root_categories_value); + + JSON_Value *totals = make_units(0, -1); + json_object_set_value(root_object, "totals", totals); + + char *serialized_string = NULL; + serialized_string = json_serialize_to_string_pretty(root_value); + if (serialized_string == NULL) { + G_fatal_error(_("Failed to initialize pretty JSON string.")); + } + puts(serialized_string); + json_free_serialized_string(serialized_string); + json_value_free(root_value); +} diff --git a/raster/r.report/prt_report.c b/raster/r.report/prt_report.c index 8edb0ded88b..d8eba602a34 100644 --- a/raster/r.report/prt_report.c +++ b/raster/r.report/prt_report.c @@ -13,83 +13,12 @@ int print_report(int unit1, int unit2) int i; int divider_level; int after_header; - int need_format; int with_stats; char *cp; int spacing; char dot; - /* examine units, determine output format */ - for (i = unit1; i <= unit2; i++) { - need_format = 1; - unit[i].label[0] = ""; - unit[i].label[1] = ""; - - switch (unit[i].type) { - case CELL_COUNTS: - need_format = 0; - unit[i].len = 5; - unit[i].label[0] = " cell"; - unit[i].label[1] = "count"; - ns = 0; - sprintf(num, "%ld", count_sum(&ns, -1)); - len = strlen(num); - if (len > unit[i].len) - unit[i].len = len; - break; - - case PERCENT_COVER: - need_format = 0; - unit[i].dp = 2; - unit[i].len = 6; - unit[i].label[0] = " % "; - unit[i].label[1] = "cover"; - unit[i].eformat = 0; - break; - - case SQ_METERS: - unit[i].label[0] = "square"; - unit[i].label[1] = "meters"; - unit[i].factor = 1.0; - break; - - case SQ_KILOMETERS: - unit[i].label[0] = " square "; - unit[i].label[1] = "kilometers"; - unit[i].factor = 1.0e-6; - break; - - case ACRES: - unit[i].label[0] = ""; - unit[i].label[1] = "acres"; - unit[i].factor = 2.47105381467165e-4; /* 640 acres in a sq mile */ - break; - - case HECTARES: - unit[i].label[0] = ""; - unit[i].label[1] = "hectares"; - unit[i].factor = 1.0e-4; - break; - - case SQ_MILES: - unit[i].label[0] = "square"; - unit[i].label[1] = " miles"; - unit[i].factor = 3.86102158542446e-7; /* 1 / ( (0.0254m/in * 12in/ft - * 5280ft/mi)^2 ) */ - break; - - default: - G_fatal_error("Unit %d not yet supported", unit[i].type); - } - if (need_format) { - unit[i].dp = 6; - unit[i].len = 10; - unit[i].eformat = 0; - ns = 0; - format_parms(area_sum(&ns, -1) * unit[i].factor, &unit[i].len, - &unit[i].dp, &(unit[i].eformat), e_format); - } - } + compute_unit_format(unit1, unit2, PLAIN); /* figure out how big the category numbers are when printed */ for (nl = 0; nl < nlayers; nl++) diff --git a/raster/r.report/r.report.html b/raster/r.report/r.report.html index d4706471e3e..f4779f27310 100644 --- a/raster/r.report/r.report.html +++ b/raster/r.report/r.report.html @@ -132,6 +132,744 @@

EXAMPLE

+-----------------------------------------------------------------------------+ +The output from r.report can be output in JSON by passing the format=json option. + +
+r.report -n -a map=towns,elevation units=miles,meters,kilometers,acres,hectares,cells,percent nsteps=2 format=json
+
+ +
+{
+    "location": "nc_spm_08_grass7",
+    "created": "2024-07-24T14:59:09+0530",
+    "region": {
+        "north": 320000,
+        "south": 10000,
+        "east": 935000,
+        "west": 120000,
+        "ew_res": 500,
+        "ns_res": 500
+    },
+    "mask": null,
+    "maps": [
+        {
+            "name": "towns",
+            "label": "South West Wake: Cities and towns derived from zipcodes",
+            "type": "raster",
+        },
+        {
+            "name": "zipcodes",
+            "label": "South West Wake: Zipcode areas derived from vector map",
+            "type": "raster",
+        }
+    ],
+    "categories": [
+        {
+            "category": 1,
+            "label": "CARY",
+            "units": [
+                {
+                    "unit": "square miles",
+                    "value": 10.231707201374819
+                },
+                {
+                    "unit": "square meters",
+                    "value": 26500000
+                },
+                {
+                    "unit": "square kilometers",
+                    "value": 26.5
+                },
+                {
+                    "unit": "acres",
+                    "value": 6548.2926088798722
+                },
+                {
+                    "unit": "hectares",
+                    "value": 2650
+                },
+                {
+                    "unit": "cell counts",
+                    "value": 106
+                },
+                {
+                    "unit": "% cover",
+                    "value": 13.086419753086419
+                }
+            ],
+            "categories": [
+                {
+                    "category": 1,
+                    "label": "from to",
+                    "range": {
+                        "from": 55.578792572021484,
+                        "to": 105.9543285369873
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 0.8687298567205034
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 2250000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 2.25
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 555.98710830112122
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 225
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 9
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 8.4905660377358494
+                        }
+                    ]
+                },
+                {
+                    "category": 2,
+                    "label": "from to",
+                    "range": {
+                        "from": 105.9543285369873,
+                        "to": 156.32986450195312
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 9.3629773446543147
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 24250000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 24.25
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 5992.305500578751
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 2425
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 97
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 91.509433962264154
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "category": 2,
+            "label": "GARNER",
+            "units": [
+                {
+                    "unit": "square miles",
+                    "value": 5.5019557592298556
+                },
+                {
+                    "unit": "square meters",
+                    "value": 14250000
+                },
+                {
+                    "unit": "square kilometers",
+                    "value": 14.25
+                },
+                {
+                    "unit": "acres",
+                    "value": 3521.2516859071011
+                },
+                {
+                    "unit": "hectares",
+                    "value": 1425
+                },
+                {
+                    "unit": "cell counts",
+                    "value": 57
+                },
+                {
+                    "unit": "% cover",
+                    "value": 7.0370370370370372
+                }
+            ],
+            "categories": [
+                {
+                    "category": 1,
+                    "label": "from to",
+                    "range": {
+                        "from": 55.578792572021484,
+                        "to": 105.9543285369873
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 4.3436492836025176
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 11250000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 11.25
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 2779.9355415056061
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 1125
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 45
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 78.94736842105263
+                        }
+                    ]
+                },
+                {
+                    "category": 2,
+                    "label": "from to",
+                    "range": {
+                        "from": 105.9543285369873,
+                        "to": 156.32986450195312
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 1.158306475627338
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 3000000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 3
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 741.31614440149497
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 300
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 12
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 21.05263157894737
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "category": 3,
+            "label": "APEX",
+            "units": [
+                {
+                    "unit": "square miles",
+                    "value": 0.9652553963561149
+                },
+                {
+                    "unit": "square meters",
+                    "value": 2500000
+                },
+                {
+                    "unit": "square kilometers",
+                    "value": 2.5
+                },
+                {
+                    "unit": "acres",
+                    "value": 617.76345366791247
+                },
+                {
+                    "unit": "hectares",
+                    "value": 250
+                },
+                {
+                    "unit": "cell counts",
+                    "value": 10
+                },
+                {
+                    "unit": "% cover",
+                    "value": 1.2345679012345678
+                }
+            ],
+            "categories": [
+                {
+                    "category": 1,
+                    "label": "from to",
+                    "range": {
+                        "from": 55.578792572021484,
+                        "to": 105.9543285369873
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 0.096525539635611488
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 250000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 0.25
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 61.776345366791247
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 25
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 1
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 10
+                        }
+                    ]
+                },
+                {
+                    "category": 2,
+                    "label": "from to",
+                    "range": {
+                        "from": 105.9543285369873,
+                        "to": 156.32986450195312
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 0.8687298567205034
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 2250000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 2.25
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 555.98710830112122
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 225
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 9
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 90
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "category": 4,
+            "label": "RALEIGH-CITY",
+            "units": [
+                {
+                    "unit": "square miles",
+                    "value": 6.0811089970435237
+                },
+                {
+                    "unit": "square meters",
+                    "value": 15750000
+                },
+                {
+                    "unit": "square kilometers",
+                    "value": 15.75
+                },
+                {
+                    "unit": "acres",
+                    "value": 3891.9097581078486
+                },
+                {
+                    "unit": "hectares",
+                    "value": 1575
+                },
+                {
+                    "unit": "cell counts",
+                    "value": 63
+                },
+                {
+                    "unit": "% cover",
+                    "value": 7.7777777777777777
+                }
+            ],
+            "categories": [
+                {
+                    "category": 1,
+                    "label": "from to",
+                    "range": {
+                        "from": 55.578792572021484,
+                        "to": 105.9543285369873
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 5.3089046799586326
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 13750000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 13.75
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 3397.6989951735186
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 1375
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 55
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 87.301587301587304
+                        }
+                    ]
+                },
+                {
+                    "category": 2,
+                    "label": "from to",
+                    "range": {
+                        "from": 105.9543285369873,
+                        "to": 156.32986450195312
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 0.7722043170848919
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 2000000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 2
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 494.21076293432998
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 200
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 8
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 12.698412698412698
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "category": 5,
+            "label": "RALEIGH-SOUTH",
+            "units": [
+                {
+                    "unit": "square miles",
+                    "value": 47.394039961085241
+                },
+                {
+                    "unit": "square meters",
+                    "value": 122750000
+                },
+                {
+                    "unit": "square kilometers",
+                    "value": 122.75
+                },
+                {
+                    "unit": "acres",
+                    "value": 30332.185575094503
+                },
+                {
+                    "unit": "hectares",
+                    "value": 12275
+                },
+                {
+                    "unit": "cell counts",
+                    "value": 491
+                },
+                {
+                    "unit": "% cover",
+                    "value": 60.617283950617285
+                }
+            ],
+            "categories": [
+                {
+                    "category": 1,
+                    "label": "from to",
+                    "range": {
+                        "from": 55.578792572021484,
+                        "to": 105.9543285369873
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 25.579268003437047
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 66250000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 66.25
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 16370.731522199681
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 6625
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 265
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 53.971486761710793
+                        }
+                    ]
+                },
+                {
+                    "category": 2,
+                    "label": "from to",
+                    "range": {
+                        "from": 105.9543285369873,
+                        "to": 156.32986450195312
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 21.814771957648198
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 56500000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 56.5
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 13961.454052894822
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 5650
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 226
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 46.028513238289207
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "category": 6,
+            "label": "RALEIGH-WEST",
+            "units": [
+                {
+                    "unit": "square miles",
+                    "value": 8.0116197897557537
+                },
+                {
+                    "unit": "square meters",
+                    "value": 20750000
+                },
+                {
+                    "unit": "square kilometers",
+                    "value": 20.75
+                },
+                {
+                    "unit": "acres",
+                    "value": 5127.4366654436735
+                },
+                {
+                    "unit": "hectares",
+                    "value": 2075
+                },
+                {
+                    "unit": "cell counts",
+                    "value": 83
+                },
+                {
+                    "unit": "% cover",
+                    "value": 10.246913580246913
+                }
+            ],
+            "categories": [
+                {
+                    "category": 1,
+                    "label": "from to",
+                    "range": {
+                        "from": 55.578792572021484,
+                        "to": 105.9543285369873
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 0.096525539635611488
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 250000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 0.25
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 61.776345366791247
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 25
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 1
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 1.2048192771084338
+                        }
+                    ]
+                },
+                {
+                    "category": 2,
+                    "label": "from to",
+                    "range": {
+                        "from": 105.9543285369873,
+                        "to": 156.32986450195312
+                    },
+                    "units": [
+                        {
+                            "unit": "square miles",
+                            "value": 7.9150942501201422
+                        },
+                        {
+                            "unit": "square meters",
+                            "value": 20500000
+                        },
+                        {
+                            "unit": "square kilometers",
+                            "value": 20.5
+                        },
+                        {
+                            "unit": "acres",
+                            "value": 5065.6603200768823
+                        },
+                        {
+                            "unit": "hectares",
+                            "value": 2050
+                        },
+                        {
+                            "unit": "cell counts",
+                            "value": 82
+                        },
+                        {
+                            "unit": "% cover",
+                            "value": 98.795180722891573
+                        }
+                    ]
+                }
+            ]
+        }
+    ],
+    "totals": [
+        {
+            "unit": "square miles",
+            "value": 78.185687104845314
+        },
+        {
+            "unit": "square meters",
+            "value": 202500000
+        },
+        {
+            "unit": "square kilometers",
+            "value": 202.5
+        },
+        {
+            "unit": "acres",
+            "value": 50038.839747100916
+        },
+        {
+            "unit": "hectares",
+            "value": 20250
+        },
+        {
+            "unit": "cell counts",
+            "value": 810
+        },
+        {
+            "unit": "% cover",
+            "value": 100
+        }
+    ]
+}
+
+

SEE ALSO

diff --git a/raster/r.report/report.c b/raster/r.report/report.c index a10a3fc34bd..1e97f33c5e2 100644 --- a/raster/r.report/report.c +++ b/raster/r.report/report.c @@ -4,18 +4,25 @@ int report(void) { int unit1, unit2; - if (stats_flag == STATS_ONLY) - return 1; + switch (format) { + case JSON: + print_json(); + break; + case PLAIN: + if (stats_flag == STATS_ONLY) + return 1; - if (nunits == 0) - print_report(0, -1); - else - for (unit1 = 0; unit1 < nunits; unit1 = unit2 + 1) { - unit2 = unit1 + 2; - if (unit2 >= nunits) - unit2 = nunits - 1; - print_report(unit1, unit2); - } + if (nunits == 0) + print_report(0, -1); + else + for (unit1 = 0; unit1 < nunits; unit1 = unit2 + 1) { + unit2 = unit1 + 2; + if (unit2 >= nunits) + unit2 = nunits - 1; + print_report(unit1, unit2); + } + break; + } return 0; } diff --git a/raster/r.report/testsuite/test_r_report.py b/raster/r.report/testsuite/test_r_report.py index 7d872e17ff8..53e49fdc174 100644 --- a/raster/r.report/testsuite/test_r_report.py +++ b/raster/r.report/testsuite/test_r_report.py @@ -9,9 +9,15 @@ for details. """ +import json import os +from itertools import zip_longest +from datetime import datetime + from grass.gunittest.case import TestCase +from grass.gunittest.gmodules import SimpleModule + class TestRasterreport(TestCase): outfile = "test_out.csv" @@ -58,6 +64,609 @@ def test_output(self): self.assertModule("r.report", map="lakes", output=self.outfile) self.assertFileExists(self.outfile) + def _assert_report_equal(self, reference, data): + keys = ["location", "region", "mask", "maps", "totals"] + for key in keys: + self.assertEqual(reference[key], data[key]) + + for category1, category2 in zip_longest( + reference["categories"], data["categories"] + ): + self.assertEqual(category1["category"], category2["category"]) + self.assertEqual(category1["label"], category2["label"]) + + for unit1, unit2 in zip_longest(category1["units"], category2["units"]): + self.assertEqual(unit1["unit"], unit2["unit"]) + self.assertAlmostEqual(unit1["value"], unit2["value"], places=6) + + for sub_category1, sub_category2 in zip_longest( + category1["categories"], category2["categories"] + ): + self.assertEqual(sub_category1["category"], sub_category2["category"]) + self.assertEqual(sub_category1["label"], sub_category2["label"]) + + for sub_unit1, sub_unit2 in zip_longest( + sub_category1["units"], sub_category2["units"] + ): + self.assertEqual(sub_unit1["unit"], sub_unit2["unit"]) + self.assertAlmostEqual( + sub_unit1["value"], sub_unit2["value"], places=6 + ) + + def test_json(self): + """Test JSON format""" + reference = { + "location": "nc_spm_full_v2alpha2", + "region": { + "north": 228500, + "south": 215000, + "east": 645000, + "west": 630000, + "ew_res": 10, + "ns_res": 10, + }, + "mask": None, + "maps": [ + { + "name": "towns", + "label": "South West Wake: Cities and towns derived from zipcodes", + "type": "raster", + }, + { + "name": "zipcodes", + "label": "South West Wake: Zipcode areas derived from vector map", + "type": "raster", + }, + ], + "categories": [ + { + "category": 1, + "label": "CARY", + "units": [ + {"unit": "cell counts", "value": 260849}, + {"unit": "% cover", "value": 12.881432098765432}, + ], + "categories": [ + { + "category": 27511, + "label": "CARY", + "units": [ + {"unit": "cell counts", "value": 105800}, + {"unit": "% cover", "value": 40.559864135956055}, + ], + }, + { + "category": 27513, + "label": "CARY", + "units": [ + {"unit": "cell counts", "value": 20530}, + {"unit": "% cover", "value": 7.8704537874402432}, + ], + }, + { + "category": 27518, + "label": "CARY", + "units": [ + {"unit": "cell counts", "value": 134519}, + {"unit": "% cover", "value": 51.569682076603705}, + ], + }, + ], + }, + { + "category": 2, + "label": "GARNER", + "units": [ + {"unit": "cell counts", "value": 141572}, + {"unit": "% cover", "value": 6.99120987654321}, + ], + "categories": [ + { + "category": 27529, + "label": "GARNER", + "units": [ + {"unit": "cell counts", "value": 141572}, + {"unit": "% cover", "value": 100}, + ], + } + ], + }, + { + "category": 3, + "label": "APEX", + "units": [ + {"unit": "cell counts", "value": 25444}, + {"unit": "% cover", "value": 1.2564938271604937}, + ], + "categories": [ + { + "category": 27539, + "label": "APEX", + "units": [ + {"unit": "cell counts", "value": 25444}, + {"unit": "% cover", "value": 100}, + ], + } + ], + }, + { + "category": 4, + "label": "RALEIGH-CITY", + "units": [ + {"unit": "cell counts", "value": 160514}, + {"unit": "% cover", "value": 7.926617283950617}, + ], + "categories": [ + { + "category": 27601, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 45468}, + {"unit": "% cover", "value": 28.326501115167524}, + ], + }, + { + "category": 27604, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 47389}, + {"unit": "% cover", "value": 29.523281458315161}, + ], + }, + { + "category": 27605, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 23677}, + {"unit": "% cover", "value": 14.750738253361078}, + ], + }, + { + "category": 27608, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 43980}, + {"unit": "% cover", "value": 27.399479173156237}, + ], + }, + ], + }, + { + "category": 5, + "label": "RALEIGH-SOUTH", + "units": [ + {"unit": "cell counts", "value": 1227632}, + {"unit": "% cover", "value": 60.623802469135804}, + ], + "categories": [ + { + "category": 27603, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 429179}, + {"unit": "% cover", "value": 34.959906551800536}, + ], + }, + { + "category": 27606, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 662642}, + {"unit": "% cover", "value": 53.977250511553954}, + ], + }, + { + "category": 27610, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 135811}, + {"unit": "% cover", "value": 11.062842936645509}, + ], + }, + ], + }, + { + "category": 6, + "label": "RALEIGH-WEST", + "units": [ + {"unit": "cell counts", "value": 208989}, + {"unit": "% cover", "value": 10.320444444444444}, + ], + "categories": [ + { + "category": 27607, + "label": "RALEIGH", + "units": [ + {"unit": "cell counts", "value": 208989}, + {"unit": "% cover", "value": 100}, + ], + } + ], + }, + ], + "totals": [ + {"unit": "cell counts", "value": 2025000}, + {"unit": "% cover", "value": 100}, + ], + } + module = SimpleModule("r.report", map="towns,zipcodes", format="json") + self.runModule(module) + data = json.loads(module.outputs.stdout) + self._assert_report_equal(reference, data) + + def test_json2(self): + """Test JSON format with more options""" + reference = { + "location": "nc_spm_full_v2alpha2", + "created": "2024-07-24T14:59:09+0530", + "region": { + "north": 228500, + "south": 215000, + "east": 645000, + "west": 630000, + "ew_res": 10, + "ns_res": 10, + }, + "mask": None, + "maps": [ + { + "name": "towns", + "label": "South West Wake: Cities and towns derived from zipcodes", + "type": "raster", + }, + { + "name": "elevation", + "label": "South-West Wake county: Elevation NED 10m", + "type": "raster", + }, + ], + "categories": [ + { + "category": 1, + "label": "CARY", + "units": [ + {"unit": "square miles", "value": 10.07143619536385}, + {"unit": "square meters", "value": 26084900}, + {"unit": "square kilometers", "value": 26.084899999999998}, + {"unit": "acres", "value": 6445.719165032852}, + {"unit": "hectares", "value": 2608.4900000000002}, + {"unit": "cell counts", "value": 260849}, + {"unit": "% cover", "value": 12.881432098765432}, + ], + "categories": [ + { + "category": 1, + "label": "from to", + "range": { + "from": 55.578792572021484, + "to": 105.9543285369873, + }, + "units": [ + {"unit": "square miles", "value": 0.9655642780829489}, + {"unit": "square meters", "value": 2500800}, + {"unit": "square kilometers", "value": 2.5008}, + {"unit": "acres", "value": 617.9611379730862}, + {"unit": "hectares", "value": 250.08}, + {"unit": "cell counts", "value": 25008}, + {"unit": "% cover", "value": 9.58715578744791}, + ], + }, + { + "category": 2, + "label": "from to", + "range": { + "from": 105.9543285369873, + "to": 156.32986450195312, + }, + "units": [ + {"unit": "square miles", "value": 9.1058719172809}, + {"unit": "square meters", "value": 23584100}, + {"unit": "square kilometers", "value": 23.5841}, + {"unit": "acres", "value": 5827.758027059766}, + {"unit": "hectares", "value": 2358.4100000000003}, + {"unit": "cell counts", "value": 235841}, + {"unit": "% cover", "value": 90.41284421255209}, + ], + }, + ], + }, + { + "category": 2, + "label": "GARNER", + "units": [ + {"unit": "square miles", "value": 5.4661254789171165}, + {"unit": "square meters", "value": 14157200}, + {"unit": "square kilometers", "value": 14.1572}, + {"unit": "acres", "value": 3498.3203065069483}, + {"unit": "hectares", "value": 1415.72}, + {"unit": "cell counts", "value": 141572}, + {"unit": "% cover", "value": 6.99120987654321}, + ], + "categories": [ + { + "category": 1, + "label": "from to", + "range": { + "from": 55.578792572021484, + "to": 105.9543285369873, + }, + "units": [ + {"unit": "square miles", "value": 4.24917008540718}, + {"unit": "square meters", "value": 11005300}, + {"unit": "square kilometers", "value": 11.0053}, + {"unit": "acres", "value": 2719.4688546605908}, + {"unit": "hectares", "value": 1100.53}, + {"unit": "cell counts", "value": 110053}, + {"unit": "% cover", "value": 77.73641680558302}, + ], + }, + { + "category": 2, + "label": "from to", + "range": { + "from": 105.9543285369873, + "to": 156.32986450195312, + }, + "units": [ + {"unit": "square miles", "value": 1.2169553935099355}, + {"unit": "square meters", "value": 3151900}, + {"unit": "square kilometers", "value": 3.1519}, + {"unit": "acres", "value": 778.8514518463573}, + {"unit": "hectares", "value": 315.19}, + {"unit": "cell counts", "value": 31519}, + {"unit": "% cover", "value": 22.263583194416974}, + ], + }, + ], + }, + { + "category": 3, + "label": "APEX", + "units": [ + {"unit": "square miles", "value": 0.9823983321953995}, + {"unit": "square meters", "value": 2544400}, + {"unit": "square kilometers", "value": 2.5444}, + {"unit": "acres", "value": 628.7349326050546}, + {"unit": "hectares", "value": 254.44000000000003}, + {"unit": "cell counts", "value": 25444}, + {"unit": "% cover", "value": 1.2564938271604937}, + ], + "categories": [ + { + "category": 1, + "label": "from to", + "range": { + "from": 55.578792572021484, + "to": 105.9543285369873, + }, + "units": [ + {"unit": "square miles", "value": 0.03262563239683668}, + {"unit": "square meters", "value": 84500}, + { + "unit": "square kilometers", + "value": 0.08449999999999999, + }, + {"unit": "acres", "value": 20.880404733975443}, + {"unit": "hectares", "value": 8.450000000000001}, + {"unit": "cell counts", "value": 845}, + {"unit": "% cover", "value": 3.321018707750354}, + ], + }, + { + "category": 2, + "label": "from to", + "range": { + "from": 105.9543285369873, + "to": 156.32986450195312, + }, + "units": [ + {"unit": "square miles", "value": 0.9497726997985628}, + {"unit": "square meters", "value": 2459900}, + { + "unit": "square kilometers", + "value": 2.4598999999999998, + }, + {"unit": "acres", "value": 607.8545278710792}, + {"unit": "hectares", "value": 245.99}, + {"unit": "cell counts", "value": 24599}, + {"unit": "% cover", "value": 96.67898129224965}, + ], + }, + ], + }, + { + "category": 4, + "label": "RALEIGH-CITY", + "units": [ + {"unit": "square miles", "value": 6.1974801876282175}, + {"unit": "square meters", "value": 16051400}, + {"unit": "square kilometers", "value": 16.0514}, + {"unit": "acres", "value": 3966.387320082052}, + {"unit": "hectares", "value": 1605.14}, + {"unit": "cell counts", "value": 160514}, + {"unit": "% cover", "value": 7.926617283950617}, + ], + "categories": [ + { + "category": 1, + "label": "from to", + "range": { + "from": 55.578792572021484, + "to": 105.9543285369873, + }, + "units": [ + {"unit": "square miles", "value": 5.062455672160989}, + {"unit": "square meters", "value": 13111700}, + { + "unit": "square kilometers", + "value": 13.111699999999999, + }, + {"unit": "acres", "value": 3239.971630183027}, + {"unit": "hectares", "value": 1311.17}, + {"unit": "cell counts", "value": 131117}, + {"unit": "% cover", "value": 81.68570965772456}, + ], + }, + { + "category": 2, + "label": "from to", + "range": { + "from": 105.9543285369873, + "to": 156.32986450195312, + }, + "units": [ + {"unit": "square miles", "value": 1.1350245154672285}, + {"unit": "square meters", "value": 2939700}, + { + "unit": "square kilometers", + "value": 2.9396999999999998, + }, + {"unit": "acres", "value": 726.415689899025}, + {"unit": "hectares", "value": 293.97}, + {"unit": "cell counts", "value": 29397}, + {"unit": "% cover", "value": 18.31429034227544}, + ], + }, + ], + }, + { + "category": 5, + "label": "RALEIGH-SOUTH", + "units": [ + {"unit": "square miles", "value": 47.39913650957801}, + {"unit": "square meters", "value": 122763200}, + {"unit": "square kilometers", "value": 122.7632}, + {"unit": "acres", "value": 30335.44736612987}, + {"unit": "hectares", "value": 12276.32}, + {"unit": "cell counts", "value": 1227632}, + {"unit": "% cover", "value": 60.6238024691358}, + ], + "categories": [ + { + "category": 1, + "label": "from to", + "range": { + "from": 55.578792572021484, + "to": 105.9543285369873, + }, + "units": [ + {"unit": "square miles", "value": 24.823086925931666}, + {"unit": "square meters", "value": 64291500}, + {"unit": "square kilometers", "value": 64.2915}, + {"unit": "acres", "value": 15886.775632596238}, + {"unit": "hectares", "value": 6429.150000000001}, + {"unit": "cell counts", "value": 642915}, + {"unit": "% cover", "value": 52.37033573579053}, + ], + }, + { + "category": 2, + "label": "from to", + "range": { + "from": 105.9543285369873, + "to": 156.32986450195312, + }, + "units": [ + {"unit": "square miles", "value": 22.576049583646338}, + {"unit": "square meters", "value": 58471700}, + {"unit": "square kilometers", "value": 58.4717}, + {"unit": "acres", "value": 14448.671733533633}, + {"unit": "hectares", "value": 5847.17}, + {"unit": "cell counts", "value": 584717}, + {"unit": "% cover", "value": 47.62966426420947}, + ], + }, + ], + }, + { + "category": 6, + "label": "RALEIGH-WEST", + "units": [ + {"unit": "square miles", "value": 8.069110401162725}, + {"unit": "square meters", "value": 20898900}, + {"unit": "square kilometers", "value": 20.898899999999998}, + {"unit": "acres", "value": 5164.230656744135}, + {"unit": "hectares", "value": 2089.8900000000003}, + {"unit": "cell counts", "value": 208989}, + {"unit": "% cover", "value": 10.320444444444444}, + ], + "categories": [ + { + "category": 1, + "label": "from to", + "range": { + "from": 55.578792572021484, + "to": 105.9543285369873, + }, + "units": [ + {"unit": "square miles", "value": 0.23822503182068916}, + {"unit": "square meters", "value": 617000}, + {"unit": "square kilometers", "value": 0.617}, + {"unit": "acres", "value": 152.4640203652408}, + {"unit": "hectares", "value": 61.7}, + {"unit": "cell counts", "value": 6170}, + {"unit": "% cover", "value": 2.9523084947054627}, + ], + }, + { + "category": 2, + "label": "from to", + "range": { + "from": 105.9543285369873, + "to": 156.32986450195312, + }, + "units": [ + {"unit": "square miles", "value": 7.8308853693420355}, + {"unit": "square meters", "value": 20281900}, + {"unit": "square kilometers", "value": 20.2819}, + {"unit": "acres", "value": 5011.766636378894}, + {"unit": "hectares", "value": 2028.19}, + {"unit": "cell counts", "value": 202819}, + {"unit": "% cover", "value": 97.04769150529454}, + ], + }, + ], + }, + ], + "totals": [ + {"unit": "square miles", "value": 78.18568710484531}, + {"unit": "square meters", "value": 202500000}, + {"unit": "square kilometers", "value": 202.5}, + {"unit": "acres", "value": 50038.839747100916}, + {"unit": "hectares", "value": 20250}, + {"unit": "cell counts", "value": 2025000}, + {"unit": "% cover", "value": 100}, + ], + } + module = SimpleModule( + "r.report", + map="towns,elevation", + units=[ + "miles", + "meters", + "kilometers", + "acres", + "hectares", + "cells", + "percent", + ], + nsteps=2, + format="json", + ) + self.runModule(module) + data = json.loads(module.outputs.stdout) + + # created field represents the time of running the command. Therefore, its exact value + # cannot be tested. We only check that it is present and in the ISO8601 datetime format + self.assertIn("created", data) + try: + # on Python 3.11 and below, datetime.fromisoformat doesn't support zone info with offset + datetime.strptime(data["created"], "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + self.fail("created field is not in isoformat: %s" % (data["created"],)) + + self._assert_report_equal(reference, data) + if __name__ == "__main__": from grass.gunittest.main import test diff --git a/raster/r.texture/main.c b/raster/r.texture/main.c index 7ff1c7d9865..6d1ec8f0458 100644 --- a/raster/r.texture/main.c +++ b/raster/r.texture/main.c @@ -312,19 +312,7 @@ int main(int argc, char *argv[]) out_set.flag_null = flag.null; out_set.flag_ind = flag.ind; - threads = atoi(parm.nproc->answer); -#if defined(_OPENMP) - /* Set the number of threads */ - omp_set_num_threads(threads); - if (threads > 1) - G_message(_("Using %d threads for parallel computing."), threads); -#else - if (threads > 1) { - G_warning(_("GRASS GIS is not compiled with OpenMP support, parallel " - "computation is disabled.")); - threads = 1; - } -#endif + threads = G_set_omp_num_threads(parm.nproc); execute_texture(data, &dim, measure_menu, measure_idx, &out_set, threads); for (i = 0; i < dim.n_outputs; i++) { @@ -332,6 +320,7 @@ int main(int argc, char *argv[]) Rast_short_history(mapname[i], "raster", &history); Rast_command_history(&history); Rast_write_history(mapname[i], &history); + Rast_free_history(&history); } /* Free allocated memory */ diff --git a/raster3d/r3.in.v5d/v5d.c b/raster3d/r3.in.v5d/v5d.c index 810238981bd..445d6f29b9f 100644 --- a/raster3d/r3.in.v5d/v5d.c +++ b/raster3d/r3.in.v5d/v5d.c @@ -467,7 +467,7 @@ static void compute_ga_gb(int nr, int nc, int nl, const float data[], /* * Compute ga, gb values for whole grid. */ - int i, lev, allmissing, num; + int i, allmissing, num; float min, max, a, b; min = 1.0e30; @@ -549,7 +549,6 @@ static void compute_ga_gb(int nr, int nc, int nl, const float data[], delt = (gridmax - gridmin) / 100000.0; if (ABS(gridmin) < delt && gridmin != 0.0 && compressmode != 4) { - float min, max; for (j = 0; j < nrncnl; j++) { if (!IS_MISSING(data[j]) && data[j] < delt) diff --git a/scripts/d.rast.leg/d.rast.leg.py b/scripts/d.rast.leg/d.rast.leg.py index 46c46928d9b..85cb423f9e9 100755 --- a/scripts/d.rast.leg/d.rast.leg.py +++ b/scripts/d.rast.leg/d.rast.leg.py @@ -104,7 +104,7 @@ def main(): # fixes trunk r64459 s = s.split(":")[1] - f = tuple([float(x) for x in s.split()]) + f = tuple(float(x) for x in s.split()) gs.run_command("d.erase") os.environ["GRASS_RENDER_FILE_READ"] = "TRUE" diff --git a/scripts/g.extension/g.extension.py b/scripts/g.extension/g.extension.py index fb399db9d06..5077f85f4f4 100644 --- a/scripts/g.extension/g.extension.py +++ b/scripts/g.extension/g.extension.py @@ -766,7 +766,7 @@ def list_available_extensions(url): def get_available_toolboxes(url): """Return toolboxes available in the repository""" tdict = {} - url = url + "toolboxes.xml" + url += "toolboxes.xml" try: tree = etree_fromurl(url) for tnode in tree.findall("toolbox"): @@ -798,7 +798,7 @@ def get_toolbox_extensions(url, name): # dictionary of extensions edict = {} - url = url + "toolboxes.xml" + url += "toolboxes.xml" try: tree = etree_fromurl(url) @@ -1275,7 +1275,7 @@ def get_toolboxes_metadata(url): def install_toolbox_xml(url, name): """Update local toolboxes metadata file""" # read metadata from remote server (toolboxes) - url = url + "toolboxes.xml" + url += "toolboxes.xml" data = get_toolboxes_metadata(url) if not data: gs.warning(_("No addons metadata available")) @@ -2243,7 +2243,7 @@ def remove_extension_files(edict, force=False): os.remove(fpath) except OSError: msg = "Unable to remove file '%s'" - err.append((_(msg) % fpath)) + err.append(_(msg) % fpath) removed = False if len(err) > 0: for error_line in err: @@ -2466,12 +2466,14 @@ def resolve_install_prefix(path, to_system): path = os.environ["GISBASE"] if path == "$GRASS_ADDON_BASE": if not os.getenv("GRASS_ADDON_BASE"): + from grass.app.runtime import get_grass_config_dir + + path = os.path.join( + get_grass_config_dir(VERSION[0], VERSION[1], os.environ), "addons" + ) gs.warning( - _( - "GRASS_ADDON_BASE is not defined, installing to ~/.grass{}/addons" - ).format(VERSION[0]) + _("GRASS_ADDON_BASE is not defined, installing to {}").format(path) ) - path = os.path.join(os.environ["HOME"], f".grass{VERSION[0]}", "addons") else: path = os.environ["GRASS_ADDON_BASE"] if os.path.exists(path) and not os.access(path, os.W_OK): @@ -2485,7 +2487,7 @@ def resolve_install_prefix(path, to_system): # ensure dir sep at the end for cases where path is used as URL and pasted # together with file names if not path.endswith(os.path.sep): - path = path + os.path.sep + path += os.path.sep os.environ["GRASS_PREFIX_ADDON_BASE"] = os.path.abspath( path ) # make likes absolute paths @@ -2514,7 +2516,7 @@ def resolve_xmlurl_prefix(url, source=None): # the exact action depends on subsequent code (somewhere) if not url.endswith("/"): - url = url + "/" + url += "/" return url diff --git a/scripts/i.oif/i.oif.py b/scripts/i.oif/i.oif.py index 4f03da94db8..cfee7890120 100755 --- a/scripts/i.oif/i.oif.py +++ b/scripts/i.oif/i.oif.py @@ -107,7 +107,7 @@ def main(): if not proc[bandp].stdout.closed: pout[bandp] = proc[bandp].communicate()[0] proc[bandp].wait() - n = n + 1 + n += 1 # wait for jobs to finish, collect the output for band in bands: diff --git a/scripts/i.pansharpen/i.pansharpen.py b/scripts/i.pansharpen/i.pansharpen.py index ff0146d7708..f344c5ac7fd 100755 --- a/scripts/i.pansharpen/i.pansharpen.py +++ b/scripts/i.pansharpen/i.pansharpen.py @@ -732,7 +732,7 @@ def matchhist(original, target, matched): stats = gs.decode(stats_out.communicate()[0]).split("\n")[:-1] stats_dict = dict(s.split(":", 1) for s in stats) total_cells = 0 # total non-null cells - for j in stats_dict: + for j in stats_dict.keys(): # noqa: PLC0206 stats_dict[j] = int(stats_dict[j]) if j != "*": total_cells += stats_dict[j] diff --git a/scripts/i.spectral/i.spectral.py b/scripts/i.spectral/i.spectral.py index 9ac8cd09bf3..d40c1076169 100755 --- a/scripts/i.spectral/i.spectral.py +++ b/scripts/i.spectral/i.spectral.py @@ -92,7 +92,7 @@ def write2textf(what, output): outf = open(output, "w") i = 0 for row in enumerate(what): - i = i + 1 + i += 1 outf.write("%d, %s\n" % (i, row)) outf.close() diff --git a/scripts/r.fillnulls/r.fillnulls.py b/scripts/r.fillnulls/r.fillnulls.py index 2627bca9ffe..a53d19a8a72 100755 --- a/scripts/r.fillnulls/r.fillnulls.py +++ b/scripts/r.fillnulls/r.fillnulls.py @@ -320,7 +320,7 @@ def main(): holename = prefix + "hole_" + cat # GTC Hole is a NULL area in a raster map gs.message(_("Filling hole %s of %s") % (hole_n, len(cat_list))) - hole_n = hole_n + 1 + hole_n += 1 # cut out only CAT hole for processing try: gs.run_command( diff --git a/scripts/r.grow/r.grow.py b/scripts/r.grow/r.grow.py index 86fdfd9ebc3..dce83b35f39 100755 --- a/scripts/r.grow/r.grow.py +++ b/scripts/r.grow/r.grow.py @@ -110,7 +110,7 @@ def main(): if metric == "euclidean": metric = "squared" - radius = radius * radius + radius *= radius # check if input file exists if not gs.find_file(input)["file"]: diff --git a/scripts/r.import/r.import.py b/scripts/r.import/r.import.py index a3f952c037b..7f13e79c336 100644 --- a/scripts/r.import/r.import.py +++ b/scripts/r.import/r.import.py @@ -289,7 +289,7 @@ def main(): env=src_env, ) gs.run_command("g.region", vector=tgtregion, env=src_env) - parameters["flags"] = parameters["flags"] + region_flag + parameters["flags"] += region_flag try: gs.run_command("r.in.gdal", env=src_env, **parameters) except CalledModuleError: diff --git a/scripts/r.in.wms/srs.py b/scripts/r.in.wms/srs.py index 460e3c14fc1..8d996fcde94 100644 --- a/scripts/r.in.wms/srs.py +++ b/scripts/r.in.wms/srs.py @@ -105,7 +105,7 @@ def getcodeurn(self): """ return "urn:%s:def:crs:%s:%s:%s" % ( - (self.naming_authority and self.naming_authority or "ogc"), + ((self.naming_authority and self.naming_authority) or "ogc"), (self.authority or ""), (self.version or ""), (self.code or ""), diff --git a/scripts/r.in.wms/wms_drv.py b/scripts/r.in.wms/wms_drv.py index 4628c60b8ef..1e7a90e9938 100644 --- a/scripts/r.in.wms/wms_drv.py +++ b/scripts/r.in.wms/wms_drv.py @@ -494,7 +494,7 @@ def __init__(self, params, bbox, region, tile_size, proj_srs, cap_file=None): self.last_tile_x = False if self.last_tile_x_size != 0: self.last_tile_x = True - self.num_tiles_x = self.num_tiles_x + 1 + self.num_tiles_x += 1 self.num_tiles_y = rows // self.tile_rows self.last_tile_y_size = rows % self.tile_rows @@ -507,7 +507,7 @@ def __init__(self, params, bbox, region, tile_size, proj_srs, cap_file=None): self.last_tile_y = False if self.last_tile_y_size != 0: self.last_tile_y = True - self.num_tiles_y = self.num_tiles_y + 1 + self.num_tiles_y += 1 self.tile_bbox = dict(self.bbox) self.tile_bbox["maxx"] = self.bbox["minx"] + self.tile_length_x @@ -816,7 +816,7 @@ def _getMatSize(self, tile_mat, mat_set_link): mat_num_bbox[i[0]] = int(i_tag.text) if i[0] in {"max_row", "max_col"}: - mat_num_bbox[i[0]] = mat_num_bbox[i[0]] - 1 + mat_num_bbox[i[0]] -= 1 break return mat_num_bbox diff --git a/scripts/r.pack/r.pack.py b/scripts/r.pack/r.pack.py index 51420b74a77..cd7d6c39282 100644 --- a/scripts/r.pack/r.pack.py +++ b/scripts/r.pack/r.pack.py @@ -130,11 +130,11 @@ def main(): # Copy vrt files if vrt_files: - for f in vrt_files.keys(): + for f, value in vrt_files.items(): f_tmp_dir = os.path.join(tmp, f) if not os.path.exists(f_tmp_dir): os.mkdir(f_tmp_dir) - path = os.path.join(vrt_files[f], element, f) + path = os.path.join(value, element, f) if os.path.exists(path): grass.debug("copying vrt file {}".format(path)) if os.path.isfile(path): diff --git a/scripts/r.reclass.area/r.reclass.area.py b/scripts/r.reclass.area/r.reclass.area.py index 1fa8a868973..5d6fc1292bc 100755 --- a/scripts/r.reclass.area/r.reclass.area.py +++ b/scripts/r.reclass.area/r.reclass.area.py @@ -184,7 +184,7 @@ def rmarea(infile, outfile, thresh, coef): # transform user input from hectares to meters because currently v.clean # rmarea accept only meters as threshold - thresh = thresh * 10000.0 + thresh *= 10000.0 vectfile = "%s_vect_%s" % (infile.split("@")[0], outfile) TMPRAST.append(vectfile) gs.run_command("r.to.vect", input=infile, output=vectfile, type="area") diff --git a/scripts/v.db.univar/v.db.univar.py b/scripts/v.db.univar/v.db.univar.py index ab604db2b71..996c965b4fb 100755 --- a/scripts/v.db.univar/v.db.univar.py +++ b/scripts/v.db.univar/v.db.univar.py @@ -94,7 +94,7 @@ def main(): if not passflags: passflags = "g" else: - passflags = passflags + "g" + passflags += "g" output_format = options["format"] try: diff --git a/scripts/v.in.wfs/v.in.wfs.py b/scripts/v.in.wfs/v.in.wfs.py index bd127c3bea0..9f53840edcf 100755 --- a/scripts/v.in.wfs/v.in.wfs.py +++ b/scripts/v.in.wfs/v.in.wfs.py @@ -127,7 +127,7 @@ def main(): wfs_url += request_base if options["name"]: - if tuple([int(x) for x in version_num.split(".")]) >= (2, 0, 0): + if tuple(int(x) for x in version_num.split(".")) >= (2, 0, 0): wfs_url += "&TYPENAMES=" + options["name"] else: wfs_url += "&TYPENAME=" + options["name"] diff --git a/scripts/v.pack/v.pack.py b/scripts/v.pack/v.pack.py index d25704b9925..cd8d96b86cd 100755 --- a/scripts/v.pack/v.pack.py +++ b/scripts/v.pack/v.pack.py @@ -40,7 +40,7 @@ from grass.script.utils import try_rmdir, try_remove from grass.script import core as grass -from grass.script import vector as vector +from grass.script import vector def cleanup(): diff --git a/temporal/t.rast.accdetect/t.rast.accdetect.py b/temporal/t.rast.accdetect/t.rast.accdetect.py index 49953246037..887616f7a1c 100644 --- a/temporal/t.rast.accdetect/t.rast.accdetect.py +++ b/temporal/t.rast.accdetect/t.rast.accdetect.py @@ -238,7 +238,7 @@ def main(): ) ) if indicator.find("@") >= 0: - indicator = indicator + indicator_id = indicator else: indicator_id = indicator + "@" + mapset diff --git a/temporal/t.rast.accumulate/t.rast.accumulate.py b/temporal/t.rast.accumulate/t.rast.accumulate.py index 9b9262a852f..3ad82fe9ed3 100644 --- a/temporal/t.rast.accumulate/t.rast.accumulate.py +++ b/temporal/t.rast.accumulate/t.rast.accumulate.py @@ -244,14 +244,6 @@ def main(): # The lower threshold space time raster dataset if lower: - if not range: - dbif.close() - gs.fatal( - _( - "You need to set the range to compute the occurrence" - " space time raster dataset" - ) - ) if lower.find("@") >= 0: lower_id = lower @@ -280,7 +272,7 @@ def main(): ) if upper.find("@") >= 0: - upper = upper + upper_id = upper else: upper_id = upper + "@" + mapset @@ -352,7 +344,7 @@ def main(): map.set_relative_time( gran_start, gran_end, input_strds.get_relative_time_unit() ) - gran_start = gran_start + granularity + gran_start += granularity gran_list.append(copy(map)) gran_list_low.append(copy(map)) gran_list_up.append(copy(map)) diff --git a/temporal/t.rast.what/t.rast.what.py b/temporal/t.rast.what/t.rast.what.py index 18a25a61080..9fa4ad5d793 100755 --- a/temporal/t.rast.what/t.rast.what.py +++ b/temporal/t.rast.what/t.rast.what.py @@ -597,9 +597,9 @@ def one_point_per_timerow_output( matrix.append(cols[:2]) if vcat: - matrix[i] = matrix[i] + cols[4:] + matrix[i] += cols[4:] else: - matrix[i] = matrix[i] + cols[3:] + matrix[i] += cols[3:] first = False diff --git a/temporal/t.unregister/t.unregister.py b/temporal/t.unregister/t.unregister.py index 492df3e9a7a..e7e234320c0 100755 --- a/temporal/t.unregister/t.unregister.py +++ b/temporal/t.unregister/t.unregister.py @@ -179,8 +179,7 @@ def main(): sp.update_command_string(dbif=dbif) elif len(update_dict) > 0: count = 0 - for key in update_dict.keys(): - id = update_dict[key] + for id in update_dict.values(): sp = tgis.open_old_stds(id, type, dbif) sp.update_from_registered_maps(dbif) gs.percent(count, len(update_dict), 1) diff --git a/temporal/t.vect.what.strds/t.vect.what.strds.py b/temporal/t.vect.what.strds/t.vect.what.strds.py index 5d29ea3b5fb..6ea36e79579 100755 --- a/temporal/t.vect.what.strds/t.vect.what.strds.py +++ b/temporal/t.vect.what.strds/t.vect.what.strds.py @@ -151,7 +151,7 @@ def main(): False, dbif, ) - aggreagated_map_name = aggreagated_map_name + "_0" + aggreagated_map_name += "_0" if new_map is None: continue # We overwrite the raster_maps list diff --git a/utils/gitlog2changelog.py b/utils/gitlog2changelog.py index e9627567e05..2626321d6fb 100755 --- a/utils/gitlog2changelog.py +++ b/utils/gitlog2changelog.py @@ -98,7 +98,7 @@ elif len(line) == 4: messageFound = True elif len(message) == 0: - message = message + line.strip() + message += line.strip() else: message = message + " " + line.strip() # If this line is hit all of the files have been stored for this commit diff --git a/vector/v.out.ogr/testsuite/test_v_out_ogr.py b/vector/v.out.ogr/testsuite/test_v_out_ogr.py new file mode 100644 index 00000000000..d269ccff606 --- /dev/null +++ b/vector/v.out.ogr/testsuite/test_v_out_ogr.py @@ -0,0 +1,115 @@ +"""Test of v.out.ogr + +@author Luís Moreira de Sousa +""" + +from pathlib import Path +from grass.gunittest.case import TestCase + + +class TestOgrExport(TestCase): + + # Vector map in NC test dataset + test_map = "boundary_county" + + # Result of import tests + temp_import = "test_ogr_import_map" + + # Column on which to test v.univar + univar_col = "PERIMETER" + + # Output of v.univar + univar_string = """n=926 +nmissing=0 +nnull=0 +min=9.64452 +max=3.70609e+06 +range=3.70608e+06 +sum=1.1223e+08 +mean=121199 +mean_abs=121199 +population_stddev=342855 +population_variance=1.17549e+11 +population_coeff_variation=2.82886 +sample_stddev=343040 +sample_variance=1.17676e+11 +kurtosis=33.681 +skewness=4.86561 +""" + + @classmethod + def setUpClass(cls): + """Use temporary region settings""" + cls.use_temp_region() + + @classmethod + def tearDownClass(cls): + """Remove the temporary region""" + cls.del_temp_region() + + def tearDown(self): + self.runModule( + "g.remove", type="vector", flags="f", pattern=f"{self.temp_import}*" + ) + for p in Path().glob(f"{self.test_map}*"): + p.unlink() + + def test_gpkg_format(self): + """Tests output to GeoPackage format""" + + self.assertModule( + "v.out.ogr", + "Export to GeoPackage Format", + input=self.test_map, + output=f"{self.test_map}.gpkg", + format="GPKG", + ) + + # Import back to verify + self.runModule( + "v.in.ogr", + input=f"{self.test_map}.gpkg", + output=self.temp_import, + ) + + self.runModule("g.region", vector=self.temp_import) + + self.assertVectorFitsUnivar( + map=self.temp_import, + reference=self.univar_string, + column=self.univar_col, + precision=1e-8, + ) + + def test_shp_format(self): + """Tests output to Shapefile format""" + + self.assertModule( + "v.out.ogr", + "Export to Shapefile Format", + input=self.test_map, + output=f"{self.test_map}.shp", + format="ESRI_Shapefile", + ) + + # Import back to verify + self.runModule( + "v.in.ogr", + input=f"{self.test_map}.shp", + output=self.temp_import, + ) + + self.runModule("g.region", vector=self.temp_import) + + self.assertVectorFitsUnivar( + map=self.temp_import, + reference=self.univar_string, + column=self.univar_col, + precision=1e-8, + ) + + +if __name__ == "__main__": + from grass.gunittest.main import test + + test() diff --git a/vector/v.univar/testsuite/test_v_univar.py b/vector/v.univar/testsuite/test_v_univar.py index 166d47a5350..766871d951c 100644 --- a/vector/v.univar/testsuite/test_v_univar.py +++ b/vector/v.univar/testsuite/test_v_univar.py @@ -167,7 +167,7 @@ def test_json(self): self.assertAlmostEqual(p1["value"], p2["value"]) self.assertCountEqual(list(expected.keys()), list(results.keys())) - for key in expected: + for key in expected.keys(): # noqa: PLC0206 self.assertAlmostEqual(expected[key], results[key])