diff --git a/include/grass/defs/raster.h b/include/grass/defs/raster.h index 7244da2a354..14bcf79e05a 100644 --- a/include/grass/defs/raster.h +++ b/include/grass/defs/raster.h @@ -392,6 +392,7 @@ int Rast_option_to_interp_type(const struct Option *); /* mask_info.c */ char *Rast_mask_info(void); +bool Rast_mask_status(char *, char *, bool *, char *, char *); int Rast__mask_info(char *, char *); /* maskfd.c */ diff --git a/lib/init/grass.py b/lib/init/grass.py index 30c0a1a68ba..3a2cb427ada 100755 --- a/lib/init/grass.py +++ b/lib/init/grass.py @@ -1727,8 +1727,8 @@ def sh_like_startup(location, location_name, grass_env_file, sh): ) ) + mask2d_test = "r.mask.status -t" # TODO: have a function and/or module to test this - mask2d_test = 'test -f "$MAPSET_PATH/cell/MASK"' mask3d_test = 'test -d "$MAPSET_PATH/grid3/RASTER3D_MASK"' specific_addition = "" diff --git a/lib/raster/mask_info.c b/lib/raster/mask_info.c index 792510879dc..6c396adc36b 100644 --- a/lib/raster/mask_info.c +++ b/lib/raster/mask_info.c @@ -1,30 +1,17 @@ -/* - ************************************************************* - * char * Rast_mask_info () +/** + * \file lib/raster/mask_info.c * - * returns a printable text of mask information + * \brief Raster Library - Get mask information * - ************************************************************ - * Rast__mask_info (name, mapset) + * (C) 1999-2024 by Vaclav Petras and the GRASS Development Team * - * char name[GNAME_MAX], mapset[GMAPSET_MAX]; + * This program is free software under the GNU General Public + * License (>=v2). Read the file COPYING that comes with GRASS + * for details. * - * function: - * determine the status off the automatic masking - * and the name of the cell file which forms the mask - * - * (the mask file is actually MASK in the current mapset, - * but is usually a reclassed cell file, and the reclass - * name and mapset are returned) - * - * returns: - * -1 no masking (name, mapset undefined) - * name, mapset are undefined - * - * 1 mask file present, masking on - * name, mapset hold mask file name, mapset - * - ***************************************************************/ + * \author CERL + * \author Vaclav Petras, NC State University, Center for Geospatial Analytics + */ #include @@ -32,6 +19,15 @@ #include #include +/** + * @brief Get a printable text with information about raster mask + * + * Determines if 2D raster mask is present and returns textual information about + * the mask suitable for end-user display. The resulting text is translated. + * Caller is responsible for freeing the memory of the returned string. + * + * @return New string with textual information + */ char *Rast_mask_info(void) { char text[GNAME_MAX + GMAPSET_MAX + 16]; @@ -53,16 +49,73 @@ char *Rast_mask_info(void) return G_store(text); } +/** + * @brief Get raster mask status information + * + * @param[out] name Name of the raster map used as mask + * @param[out] mapset Mapset name of the map + * @param[out] is_mask_reclass True, if MASK raster is a reclass + * + * @return true if mask is present, false otherwise + */ +bool Rast_mask_status(char *name, char *mapset, bool *is_mask_reclass, + char *reclass_name, char *reclass_mapset) +{ + int present = Rast__mask_info(name, mapset); + + if (is_mask_reclass && reclass_name && reclass_mapset) { + if (present) { + *is_mask_reclass = Rast_is_reclass("MASK", G_mapset(), reclass_name, + reclass_mapset) > 0; + // The original mask values are overwritten, put back the original + // values, so that we can report them to the caller. + strcpy(name, "MASK"); + strcpy(mapset, G_mapset()); + } + else { + *is_mask_reclass = false; + } + } + + if (present == 1) + return true; + else + return false; +} + +/** + * @brief Get information about the current mask + * + * Determines the status of the automatic masking and the name of the 2D + * raster which forms the mask. Typically, mask is raster called MASK in the + * current mapset, but when used with r.mask, it is usually a reclassed + * raster, and so when a MASK raster is present and it is a reclass raster, + * the name and mapset of the underlying reclassed raster are returned. + * + * The name and mapset is written to the parameter which need to be defined + * with a sufficient size, least as `char name[GNAME_MAX], mapset[GMAPSET_MAX]`. + * + * When the masking is not active, -1 is returned and name and mapset are + * undefined. When the masking is active, 1 is returned and name and mapset + * will hold the name and mapset of the underlying raster. + * + * @param[out] name Name of the raster map used as mask + * @param[out] mapset Name of the map's mapset + * + * @return 1 if mask is present, -1 otherwise + */ int Rast__mask_info(char *name, char *mapset) { char rname[GNAME_MAX], rmapset[GMAPSET_MAX]; - strcpy(name, "MASK"); - strcpy(mapset, G_mapset()); + strcpy(rname, "MASK"); + strcpy(rmapset, G_mapset()); - if (!G_find_raster(name, mapset)) + if (!G_find_raster(rname, rmapset)) return -1; + strcpy(name, rname); + strcpy(mapset, rmapset); if (Rast_is_reclass(name, mapset, rname, rmapset) > 0) { strcpy(name, rname); strcpy(mapset, rmapset); diff --git a/raster/Makefile b/raster/Makefile index bcd07660238..91ff54d0863 100644 --- a/raster/Makefile +++ b/raster/Makefile @@ -45,6 +45,7 @@ SUBDIRS = \ r.lake \ r.li \ r.mapcalc \ + r.mask.status \ r.mfilter \ r.mode \ r.neighbors \ diff --git a/raster/r.mask.status/Makefile b/raster/r.mask.status/Makefile new file mode 100644 index 00000000000..62c968d044e --- /dev/null +++ b/raster/r.mask.status/Makefile @@ -0,0 +1,10 @@ +MODULE_TOPDIR = ../.. + +PGM = r.mask.status + +LIBES = $(MANAGELIB) $(RASTERLIB) $(GISLIB) $(PARSONLIB) +DEPENDENCIES = $(MANAGEDEP) $(RASTERDEP) $(GISDEP) + +include $(MODULE_TOPDIR)/include/Make/Module.make + +default: cmd diff --git a/raster/r.mask.status/main.c b/raster/r.mask.status/main.c new file mode 100644 index 00000000000..b6e251aa868 --- /dev/null +++ b/raster/r.mask.status/main.c @@ -0,0 +1,184 @@ +/**************************************************************************** + * + * MODULE: r.mask.status + * AUTHORS: Vaclav Petras + * PURPOSE: Report status of raster mask + * COPYRIGHT: (C) 2024 by Vaclav Petras and 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. + * + *****************************************************************************/ + +#include +#include +#include +#include + +#include +#include +#include +#include + +struct Parameters { + struct Option *format; + struct Flag *like_test; +}; + +void parse_parameters(struct Parameters *params, int argc, char **argv) +{ + struct GModule *module; + + module = G_define_module(); + G_add_keyword(_("raster")); + G_add_keyword(_("mask")); + G_add_keyword(_("reclassification")); + module->label = _("Reports presence or absence of a raster mask"); + module->description = + _("Provides information about present of a 2D raster mask" + " as text output or return code"); + + params->format = G_define_option(); + params->format->key = "format"; + params->format->type = TYPE_STRING; + params->format->required = NO; + params->format->answer = "plain"; + params->format->options = "plain,json,bash,yaml"; + params->format->description = _("Format for reporting"); + + params->like_test = G_define_flag(); + params->like_test->key = 't'; + params->like_test->label = + _("Return code 0 when mask present, 1 otherwise"); + params->like_test->description = + _("Behave like the test utility, 0 for true, 1 for false, no output"); + // suppress_required is not required given the default value for format. + + if (G_parser(argc, argv)) + exit(EXIT_FAILURE); +} + +int report_status(struct Parameters *params) +{ + + char name[GNAME_MAX]; + char mapset[GMAPSET_MAX]; + char reclass_name[GNAME_MAX]; + char reclass_mapset[GMAPSET_MAX]; + + bool is_mask_reclass; + bool present = Rast_mask_status(name, mapset, &is_mask_reclass, + reclass_name, reclass_mapset); + + // This does not have to be exclusive with the printing, but perhaps there + // is a different boolean flag which does the return code and printing and + // this really behaves like the test utility facilitate the primary usage of + // this which is prompt building (there any output would be noise). + if (params->like_test->answer) { + if (present) + return 0; + return 1; + } + + // Mask raster + char *full_mask = G_fully_qualified_name(name, mapset); + // Underlying raster if applicable + char *full_underlying = NULL; + if (is_mask_reclass) + full_underlying = G_fully_qualified_name(reclass_name, reclass_mapset); + + if (strcmp(params->format->answer, "json") == 0) { + JSON_Value *root_value = json_value_init_object(); + JSON_Object *root_object = json_object(root_value); + json_object_set_boolean(root_object, "present", present); + if (present) + json_object_set_string(root_object, "full_name", full_mask); + else + json_object_set_null(root_object, "full_name"); + if (is_mask_reclass) + json_object_set_string(root_object, "is_reclass_of", + full_underlying); + else + json_object_set_null(root_object, "is_reclass_of"); + char *serialized_string = json_serialize_to_string_pretty(root_value); + puts(serialized_string); + json_free_serialized_string(serialized_string); + json_value_free(root_value); + } + else if (strcmp(params->format->answer, "bash") == 0) { + printf("present="); + if (present) + printf("1"); // Good choice here or not? + else + printf("0"); + printf("\nfull_name="); + if (present) + printf("%s", full_mask); + printf("\nis_reclass_of="); + if (is_mask_reclass) + printf("%s", full_underlying); + printf("\n"); + } + else if (strcmp(params->format->answer, "yaml") == 0) { + printf("present: "); + if (present) + printf("true"); + else + printf("false"); + printf("\nfull_name: "); + if (present) + printf("|-\n %s", full_mask); + else + printf("null"); + // Null values in YAML can be an empty (no) value (rather than null), + // so we could use that. + printf("\nis_reclass_of: "); + // Using block scalar with |- to avoid need for escaping. + // Alternatively, we could check mapset naming limits against YAML + // escaping needs for different types of strings. + if (is_mask_reclass) + printf("|-\n %s", full_underlying); + else + printf("null"); + // We could also produce true if is reclass, false otherwise. + // printf("\nmask_reclass: "); + // if (is_mask_reclass) + // printf("true"); + // else + // printf("false"); + // We could also outputting mask cats to inform user about the + // relevant portion of the map, but that should be done by accessing + // the actual mask anyway. + printf("\n"); + } + else { + if (present) + printf(_("Mask is active")); + else + printf(_("Mask is not present")); + if (present) { + printf("\n"); + printf(_("Mask name: %s"), full_mask); + } + if (is_mask_reclass) { + printf("\n"); + printf(_("Mask is a raster reclassified from: %s"), + full_underlying); + } + printf("\n"); + } + + G_free(full_mask); + G_free(full_underlying); + return EXIT_SUCCESS; +} + +int main(int argc, char **argv) +{ + struct Parameters params; + + G_gisinit(argv[0]); + parse_parameters(¶ms, argc, argv); + return report_status(¶ms); +} diff --git a/raster/r.mask.status/r.mask.status.html b/raster/r.mask.status/r.mask.status.html new file mode 100644 index 00000000000..b4f40f9087b --- /dev/null +++ b/raster/r.mask.status/r.mask.status.html @@ -0,0 +1,56 @@ +

DESCRIPTION

+ +r.mask.status reports presence or absence of a raster mask. + +

EXAMPLES

+ +

Generate JSON output

+ +To generate JSON output, use the format option: + +
+r.mask.status format=json
+
+ +In Python: + +
+import grass.script as gs
+gs.parse_command("r.mask.status", format="json")
+
+ +This returns a dictionary with keys present, +full_name, and is_reclass_of. + +

Use as the test utility

+ +The POSIX test utility uses return code 0 to indicate presence +and 1 to indicate absence of a file, so testing existence of a file with +test -f gives return code 0 when the file exists. +r.mask.status can be used in the same with the the -t flag: + +
+r.mask.status -t
+
+ +In a Bash script: + +
+# Bash
+if r.mask.status -t; then
+    echo "Masking is active"
+else
+    echo "Masking is not active"
+fi
+
+ +

SEE ALSO

+ + +r.mask, +g.region + + +

AUTHORS

+ +Vaclav Petras, NC State University, Center for Geospatial Analytics diff --git a/raster/r.mask.status/tests/conftest.py b/raster/r.mask.status/tests/conftest.py new file mode 100644 index 00000000000..5a930158bd0 --- /dev/null +++ b/raster/r.mask.status/tests/conftest.py @@ -0,0 +1,25 @@ +"""Fixtures for simple sessions""" + +import os +import pytest +import grass.script as gs + + +@pytest.fixture() +def session_no_data(tmp_path): + """Set up a GRASS session for the tests.""" + project = "test_project" + gs.create_project(tmp_path, project) + with gs.setup.init(tmp_path / project, env=os.environ.copy()) as session: + yield session + + +@pytest.fixture() +def session_with_data(tmp_path): + """Set up a GRASS session for the tests.""" + project = tmp_path / "test_project" + gs.create_project(project) + with gs.setup.init(project, env=os.environ.copy()) as session: + gs.run_command("g.region", rows=2, cols=2, env=session.env) + gs.mapcalc("a = 1", env=session.env) + yield session diff --git a/raster/r.mask.status/tests/r_mask_status_test.py b/raster/r.mask.status/tests/r_mask_status_test.py new file mode 100644 index 00000000000..37b556b30e0 --- /dev/null +++ b/raster/r.mask.status/tests/r_mask_status_test.py @@ -0,0 +1,126 @@ +"""Tests of r.mask.status""" + +import pytest + +try: + import yaml +except ImportError: + yaml = None + +import grass.script as gs + + +def test_json_no_mask(session_no_data): + """Check JSON format for no mask""" + session = session_no_data + data = gs.parse_command("r.mask.status", format="json", env=session.env) + assert "present" in data + assert "full_name" in data + assert "is_reclass_of" in data + assert data["present"] is False + assert not data["full_name"] + assert not data["is_reclass_of"] + + +def test_json_with_r_mask(session_with_data): + """Check JSON format for the r.mask case""" + session = session_with_data + gs.run_command("r.mask", raster="a", env=session.env) + data = gs.parse_command("r.mask.status", format="json", env=session.env) + assert data["present"] is True + assert data["full_name"] == "MASK@PERMANENT" + assert data["is_reclass_of"] == "a@PERMANENT" + # Now remove the mask. + gs.run_command("r.mask", flags="r", env=session.env) + data = gs.parse_command("r.mask.status", format="json", env=session.env) + assert data["present"] is False + assert not data["full_name"] + assert not data["is_reclass_of"] + + +def test_json_with_g_copy(session_with_data): + """Check JSON format for the low-level g.copy case""" + session = session_with_data + gs.run_command("g.copy", raster="a,MASK", env=session.env) + data = gs.parse_command("r.mask.status", format="json", env=session.env) + assert data["present"] is True + assert data["full_name"] == "MASK@PERMANENT" + assert not data["is_reclass_of"] + # Now remove the mask. + gs.run_command("g.remove", type="raster", name="MASK", flags="f", env=session.env) + data = gs.parse_command("r.mask.status", format="json", env=session.env) + assert data["present"] is False + assert not data["full_name"] + assert not data["is_reclass_of"] + + +def test_shell(session_with_data): + """Check shell format for the r.mask case""" + session = session_with_data + gs.run_command("r.mask", raster="a", env=session.env) + data = gs.parse_command("r.mask.status", format="bash", env=session.env) + assert int(data["present"]) + assert data["full_name"] == "MASK@PERMANENT" + assert data["is_reclass_of"] == "a@PERMANENT" + # Now remove the mask. + gs.run_command("r.mask", flags="r", env=session.env) + data = gs.parse_command("r.mask.status", format="bash", env=session.env) + assert not int(data["present"]) + assert not data["full_name"] + assert not data["is_reclass_of"] + + +@pytest.mark.skipif(yaml is None, reason="PyYAML package not available") +def test_yaml(session_with_data): + """Check YAML format for the r.mask case""" + session = session_with_data + gs.run_command("r.mask", raster="a", env=session.env) + text = gs.read_command("r.mask.status", format="yaml", env=session.env) + data = yaml.safe_load(text) + assert data["present"] is True + assert data["full_name"] == "MASK@PERMANENT" + assert data["is_reclass_of"] == "a@PERMANENT" + # Now remove the mask. + gs.run_command("r.mask", flags="r", env=session.env) + text = gs.read_command("r.mask.status", format="yaml", env=session.env) + data = yaml.safe_load(text) + assert data["present"] is False + assert not data["full_name"] + assert not data["is_reclass_of"] + + +def test_plain(session_with_data): + """Check plain text format for the r.mask case""" + session = session_with_data + gs.run_command("r.mask", raster="a", env=session.env) + text = gs.read_command("r.mask.status", format="plain", env=session.env) + assert text + assert "MASK@PERMANENT" in text + assert "a@PERMANENT" in text + # Now remove the mask. + gs.run_command("r.mask", flags="r", env=session.env) + text = gs.read_command("r.mask.status", format="plain", env=session.env) + assert text + + +def test_without_parameters(session_no_data): + """Check output is generated with no parameters""" + session = session_no_data + text = gs.read_command("r.mask.status", env=session.env) + assert text + + +def test_behavior_mimicking_test_program(session_with_data): + """Check test program like behavior for the r.mask case""" + session = session_with_data + gs.run_command("r.mask", raster="a", env=session.env) + returncode = gs.run_command( + "r.mask.status", flags="t", env=session.env, errors="status" + ) + assert returncode == 0 + # Now remove the mask. + gs.run_command("r.mask", flags="r", env=session.env) + returncode = gs.run_command( + "r.mask.status", flags="t", env=session.env, errors="status" + ) + assert returncode == 1