diff --git a/doc/development/style_guide.md b/doc/development/style_guide.md index 6f38adbbf13..7d0e9bfe31c 100644 --- a/doc/development/style_guide.md +++ b/doc/development/style_guide.md @@ -463,15 +463,21 @@ The `--overwrite` flag can be globally enabled by setting the environment variab #### Mask GRASS GIS has a global mask managed by the _r.mask_ tool and represented by a -raster called MASK. Raster tools called as a subprocess will automatically +raster called MASK by default. Raster tools called as a subprocess will automatically respect the globally set mask when reading the data. For outputs, respecting of the mask is optional. -Tools **should not set or remove the global mask**. If the tool cannot avoid -setting the mask internally, it should check for presence of the mask and fail -if the mask is present. The tools should not remove and later restore the -original mask because that creates confusing behavior for interactive use and -breaks parallel processing. +Tools should generally respect the global mask set by a user. If the mask set by the +user is not respected by a tool, the exact behavior should be described in the +documentation. On the other hand, ignoring mask is usually the desired behavior +for import tools which corresponds with the mask being applied only when reading +existing raster data in a project. + +Tools **should not set or remove the global mask** to prevent unintended +behavior during interactive sessions and to maintain parallel processing +integrity. If a tool requires a mask for its operation, it should implement +a temporary mask using _MaskManager_ in Python or by setting the `GRASS_MASK` +environment variable. Generally, any mask behavior should be documented unless it is the standard case where masked cells do not participate in the computation and are represented as @@ -577,6 +583,76 @@ gs.run_command("r.slope.aspect", elevation=input_raster, slope=slope, env=env) This approach makes the computational region completely safe for parallel processes as no region-related files are modified. +#### Changing raster mask + +The _MaskManager_ in Python API provides a way for tools to change, or possibly +to ignore, a raster mask for part of the computation. + +In the following example, _MaskManager_ modifies the global system environment +for the tool (aka _os.environ_) so that custom mask can be applied: + +```python +# Previously user-set mask applies here (if any). +gs.run_command("r.slope.aspect", elevation=input_raster, aspect=aspect) + +with gs.MaskManager(): + # Only the mask we set here will apply. + gs.run_command("r.mask", raster=mask_raster) + gs.run_command("r.slope.aspect", elevation=input_raster, slope=slope) +# Mask is disabled and the mask raster is removed at the end of the with block. + +# Previously user-set mask applies here again. +``` + +Because tools should generally respect the provided mask, the mask in a tool +should act as an additional mask. This can be achieved when preparing the new +mask raster using a tool which reads an existing raster: + +```python +# Here we create an initial mask by creating a raster from vector, +# but that does not use mask. +gs.run_command( + "v.to.rast", input=input_vector, where="name == 'Town'", output=town_boundary +) +# So, we use a raster algebra expression. Mask will be applied if set +# because in the expression, we are reading an existing raster. +gs.mapcalc(f"{raster_mask} = {town_boundary}") + +with gs.MaskManager(): + gs.run_command("r.mask", raster=mask_raster) + # Both user mask and the town_boundary are used here. + gs.run_command("r.slope.aspect", elevation=input_raster, slope=slope) +``` + +To disable the mask, which may be needed in processing steps of import tool, +we can do: + +```python +# Mask applies here if set. +gs.run_command("r.slope.aspect", elevation=input_raster, aspect=aspect) + +with gs.MaskManager(): + # No mask was set in this context, so the tool runs without a mask. + gs.run_command("r.slope.aspect", elevation=input_raster, slope=slope) + +# Mask applies again. +``` + +If needed, tools can implement optional support for a user-set raster mask by +passing or not passing the current name of a mask obtained from _r.mask.status_ +and by preparing the internal mask raster beforehand with the user mask active. + +If different subprocesses, running in parallel, use different masks, +it is best to create mask rasters beforehand (to avoid limitations of _r.mask_ and +the underlying _r.reclass_ tool). The name of the mask raster can then be passed to +the manager: + +```python +env = os.environ.copy() +with gs.MaskManager(mask_name=mask_raster, env=env): + gs.run_command("r.slope.aspect", elevation=input_raster, slope=slope, env=env) +``` + #### Temporary Maps Using temporary maps is preferred over using temporary mapsets. This follows the diff --git a/doc/examples/notebooks/parallelization_tutorial.ipynb b/doc/examples/notebooks/parallelization_tutorial.ipynb index 32f30d78a74..c3a1b54accf 100644 --- a/doc/examples/notebooks/parallelization_tutorial.ipynb +++ b/doc/examples/notebooks/parallelization_tutorial.ipynb @@ -459,6 +459,73 @@ "\n", "Alternatively, a PostgreSQL or another database backend can be used for attributes to offload the parallel writing to the database system." ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "df444278", + "metadata": {}, + "source": [ + "#### Safely modifying raster mask in a single mapset" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a8d52c4c", + "metadata": {}, + "source": [ + "Mask is by default specified per-mapset and shared by all the processes. Additionally, *r.mask* is using *r.reclass* in the background which may cause issues if the mask is derived from the same base map in parallel. The use of *MaskManager* in the following example allows each process to use a different raster. The raster is used directly as a mask to avoid the need to use *r.mask*.\n", + "\n", + "The following code derives basins (watersheds) based on a threshold from the digital elevation model. Then, for each basin, it computes the topographic index with *r.topidx*." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f72dcca", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile example.py\n", + "elevation = \"elev_state_500m\"\n", + "gs.run_command(\"g.region\", raster=elevation)\n", + "gs.run_command(\"r.watershed\", elevation=elevation, basin=\"basins\", threshold=10000)\n", + "\n", + "cats = gs.parse_command(\"r.describe\", map=\"basins\", flags=\"1n\", format=\"json\")[\"values\"]\n", + "\n", + "def topidx(cat):\n", + " # Define output name and mask name.\n", + " output = f\"topidx_{cat}\"\n", + " basin = f\"basin_{cat}\"\n", + " # Extract subwatershed by category into separate raster, creating\n", + " # a 0-or-1 mask which has no NULLs (although NULLs in mask are allowed).\n", + " gs.mapcalc(f\"{basin} = if(isnull(basins), 0, if(basins == {cat}, 1, 0))\")\n", + " # Create a copy of the environment for this process before modifying it.\n", + " env = os.environ.copy()\n", + " # Set the computational region to match non-null area in the new raster.\n", + " env[\"GRASS_REGION\"] = gs.region_env(raster=\"basins\", zoom=basin, env=env)\n", + " # Use mask context manager to specify which raster to use as a mask\n", + " # and pass the environment we are using.\n", + " with gs.MaskManager(mask_name=basin, env=env):\n", + " # Run actual computation with active mask.\n", + " gs.run_command(\"r.topidx\", input=elevation, output=output, env=env)\n", + " return output\n", + "\n", + "with Pool(processes=4) as pool:\n", + " outputs = pool.map(topidx, cats)\n", + " print(outputs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c6cfb3b", + "metadata": {}, + "outputs": [], + "source": [ + "%run example.py" + ] } ], "metadata": { diff --git a/lib/init/variables.html b/lib/init/variables.html index 4bd369abf4b..b1ff2d4e108 100644 --- a/lib/init/variables.html +++ b/lib/init/variables.html @@ -512,6 +512,11 @@

List of selected internal GRASS environment variables

This allows programs such as the GUI to run external commands on an alternate region without having to modify the WIND file then change it back afterwards. + +
GRASS_MASK
+
[libgis]
+ use the raster map specified by name as mask, instead of a raster called + MASK in the current mapset.

List of selected GRASS gisenv variables

diff --git a/lib/raster/auto_mask.c b/lib/raster/auto_mask.c index 0f4717a7544..1c23cd19a9b 100644 --- a/lib/raster/auto_mask.c +++ b/lib/raster/auto_mask.c @@ -3,14 +3,13 @@ * * \brief Raster Library - Auto masking routines. * - * (C) 2001-2008 by the GRASS Development Team + * (C) 2001-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. * * \author GRASS GIS Development Team - * - * \date 1999-2008 + * \author Vaclav Petras (environmental variable and refactoring) */ #include @@ -31,7 +30,6 @@ * \return 0 if mask unset or unavailable * \return 1 if mask set and available and ready to use */ - int Rast__check_for_auto_masking(void) { struct Cell_head cellhd; @@ -39,21 +37,26 @@ int Rast__check_for_auto_masking(void) Rast__init(); /* if mask is switched off (-2) return -2 - if R__.auto_mask is not set (-1) or set (>=0) recheck the MASK */ + if R__.auto_mask is not set (-1) or set (>=0) recheck the mask */ + // TODO: This needs to be documented or modified accordingly. if (R__.auto_mask < -1) return R__.auto_mask; /* if(R__.mask_fd > 0) G_free (R__.mask_buf); */ - /* look for the existence of the MASK file */ - R__.auto_mask = (G_find_raster("MASK", G_mapset()) != 0); + /* Decide between default mask name and env var specified one. */ + char *mask_name = Rast_mask_name(); + char *mask_mapset = ""; + + /* Check for the existence of the mask raster. */ + R__.auto_mask = (G_find_raster2(mask_name, mask_mapset) != 0); if (R__.auto_mask <= 0) return 0; - /* check MASK projection/zone against current region */ - Rast_get_cellhd("MASK", G_mapset(), &cellhd); + /* Check mask raster projection/zone against current region */ + Rast_get_cellhd(mask_name, mask_mapset, &cellhd); if (cellhd.zone != G_zone() || cellhd.proj != G_projection()) { R__.auto_mask = 0; return 0; @@ -61,16 +64,17 @@ int Rast__check_for_auto_masking(void) if (R__.mask_fd >= 0) Rast_unopen(R__.mask_fd); - R__.mask_fd = Rast__open_old("MASK", G_mapset()); + R__.mask_fd = Rast__open_old(mask_name, mask_mapset); if (R__.mask_fd < 0) { R__.auto_mask = 0; - G_warning(_("Unable to open automatic MASK file")); + G_warning(_("Unable to open automatic mask <%s>"), mask_name); return 0; } /* R__.mask_buf = Rast_allocate_c_buf(); */ R__.auto_mask = 1; + G_free(mask_name); return 1; } diff --git a/lib/raster/get_row.c b/lib/raster/get_row.c index d95eadc6e2f..8e2e70003eb 100644 --- a/lib/raster/get_row.c +++ b/lib/raster/get_row.c @@ -764,7 +764,7 @@ void Rast_get_d_row_nomask(int fd, DCELL *buf, int row) * two particular types check the functions). * - Step 4: read or simmulate null value row and zero out cells * corresponding to null value cells. The masked out cells are set to null when - * the mask exists. (the MASK is taken care of by null values (if the null file + * the mask exists. (the mask is taken care of by null values (if the null file * doesn't exist for this map, then the null row is simulated by assuming that * all zero are nulls *** in case of Rast_get_row() and assuming that all data * is valid in case of G_get_f/d_raster_row(). In case of deprecated function @@ -1089,7 +1089,7 @@ static void embed_nulls(int fd, void *buf, int row, RASTER_MAP_TYPE map_type, Read or simulate null value row and set the cells corresponding to null value to 1. The masked out cells are set to null when the - mask exists. (the MASK is taken care of by null values + mask exists. (the mask is taken care of by null values (if the null file doesn't exist for this map, then the null row is simulated by assuming that all zeros in raster map are nulls. Also all masked out cells become nulls. diff --git a/lib/raster/mask_info.c b/lib/raster/mask_info.c index 25d61341ebe..c7e7d7788d6 100644 --- a/lib/raster/mask_info.c +++ b/lib/raster/mask_info.c @@ -14,6 +14,7 @@ */ #include +#include #include #include @@ -27,6 +28,8 @@ * Caller is responsible for freeing the memory of the returned string. * * @return New string with textual information + * + * @see Rast_mask_status() */ char *Rast_mask_info(void) { @@ -53,10 +56,12 @@ char *Rast_mask_info(void) * @brief Retrieves the name of the raster mask to use. * * The returned raster map name is fully qualified, i.e., in the form - % "name@mapset". + * "name@mapset". The mask name is returned whether the mask is present or not. * - * The mask name is "MASK@", where is the current - * mapset. + * This function checks if an environment variable "GRASS_MASK" is set. + * If it is set, the value of the environment variable is returned + * as the mask name. If it is not set, the function will default to the + * mask name "MASK@", where is the current mapset. * * The memory for the returned mask name is dynamically allocated using * G_store(). It is the caller's responsibility to free the memory with @@ -66,10 +71,44 @@ char *Rast_mask_info(void) */ char *Rast_mask_name(void) { - // Mask name is always "MASK@". + // First, see if the environment variable is defined. + const char *env_variable = getenv("GRASS_MASK"); + if (env_variable != NULL && strcmp(env_variable, "") != 0) { + // Variable exists and is not empty. + // While the function does not document that, the provided mapset + // is a fallback, so we don't have to parse the name to find out + // ourselves what to do. + return G_fully_qualified_name(env_variable, G_mapset()); + } + + // Mask name defaults to "MASK@". return G_fully_qualified_name("MASK", G_mapset()); } +/** + * @brief Get name of a mask if it is present + * + * Unlike, Rast__mask_info() this always returns name of the mask + * if it is present regardless of the mask being a reclass or not. + * + * @param[out] name Name of the raster map used as mask + * @param[out] mapset Name of the map's mapset + * + * @return true if mask is present, false otherwise + */ +static bool Rast__get_present_mask(char *name, char *mapset) +{ + char rname[GNAME_MAX], rmapset[GMAPSET_MAX]; + char *full_name = Rast_mask_name(); + if (!G_find_raster2(full_name, "")) + return false; + G_unqualified_name(full_name, "", rname, rmapset); + strncpy(name, rname, GMAPSET_MAX); + strncpy(mapset, rmapset, GMAPSET_MAX); + G_free(full_name); + return true; +} + /** * @brief Get raster mask status information * @@ -94,29 +133,18 @@ char *Rast_mask_name(void) 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); + bool present = Rast__get_present_mask(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; - if (*is_mask_reclass) { - // The original mask values were overwritten in the initial - // info call. Put back the original values, so that we can - // report them to the caller. - strcpy(name, "MASK"); - strcpy(mapset, G_mapset()); - } + *is_mask_reclass = + Rast_is_reclass(name, mapset, reclass_name, reclass_mapset) > 0; } else { *is_mask_reclass = false; } } - - if (present == 1) - return true; - else - return false; + return present; } /** @@ -125,7 +153,7 @@ bool Rast_mask_status(char *name, char *mapset, bool *is_mask_reclass, * 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, + * 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 @@ -139,24 +167,20 @@ bool Rast_mask_status(char *name, char *mapset, bool *is_mask_reclass, * @param[out] mapset Name of the map's mapset * * @return 1 if mask is present, -1 otherwise + * + * @see Rast_mask_status(), Rast_mask_name() */ int Rast__mask_info(char *name, char *mapset) { char rname[GNAME_MAX], rmapset[GMAPSET_MAX]; - - strcpy(rname, "MASK"); - (void)G_strlcpy(rmapset, G_mapset(), GMAPSET_MAX); - - if (!G_find_raster(rname, rmapset)) + bool present = Rast__get_present_mask(name, mapset); + if (!present) return -1; - strcpy(name, rname); - strcpy(mapset, rmapset); if (Rast_is_reclass(name, mapset, rname, rmapset) > 0) { strcpy(name, rname); strcpy(mapset, rmapset); } - return 1; } @@ -167,5 +191,7 @@ int Rast__mask_info(char *name, char *mapset) */ bool Rast_mask_is_present(void) { - return G_find_raster("MASK", G_mapset()) != NULL; + char *name = Rast_mask_name(); + bool present = G_find_raster2(name, "") != NULL; + return present; } diff --git a/lib/raster/maskfd.c b/lib/raster/maskfd.c index 5a01739bf35..2d639edb027 100644 --- a/lib/raster/maskfd.c +++ b/lib/raster/maskfd.c @@ -1,14 +1,15 @@ /*! * \file lib/raster/maskfd.c * - * \brief Raster Library - Mask functions. + * \brief Raster Library - Mask file descriptor and state. * - * (C) 2001-2009 by the GRASS Development Team + * (C) 2001-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. * * \author Original author CERL + * \author Vaclav Petras (documentation) */ #include @@ -17,12 +18,15 @@ #include "R.h" /*! - * \brief Test for MASK. + * \brief Test for raster mask presence and get file descriptor if present. * - * \return -1 if no MASK - * \return file descriptor if MASK + * This function tests the mask presence and takes into account the state of + * auto-masking in the library, so mask is considered as not present when + * masking is suppressed regardless of the presence of the mask raster. + * + * \return -1 if mask is not present + * \return file descriptor if raster mask is present and active */ - int Rast_maskfd(void) { Rast__check_for_auto_masking(); diff --git a/lib/raster/open.c b/lib/raster/open.c index d547373574f..da6682144c7 100644 --- a/lib/raster/open.c +++ b/lib/raster/open.c @@ -120,14 +120,14 @@ int Rast_open_old(const char *name, const char *mapset) R__.mask_buf = Rast_allocate_c_buf(); now we don't ever free it!, so no need to allocate it (Olga) */ - /* mask_buf is used for reading MASK file when mask is set and + /* mask_buf is used for reading mask file when mask is set and for reading map rows when the null file doesn't exist */ return fd; } /*! \brief Lower level function, open cell files, supercell files, - and the MASK file. + and the mask file. Actions: - opens the named cell file, following reclass reference if @@ -140,7 +140,7 @@ int Rast_open_old(const char *name, const char *mapset) are left to the calling routine since the masking logic will want to issue a different warning. - Note: This routine does NOT open the MASK layer. If it did we would + Note: This routine does NOT open the mask layer. If it did we would get infinite recursion. This routine is called to open the mask by Rast__check_for_auto_masking() which is called by Rast_open_old(). diff --git a/lib/raster/rasterlib.dox b/lib/raster/rasterlib.dox index 5833abac148..baee0d081da 100644 --- a/lib/raster/rasterlib.dox +++ b/lib/raster/rasterlib.dox @@ -974,15 +974,15 @@ current region. Reads a row from NULL value bitmap file for the raster map open for read. If there is no bitmap file, then this routine simulates the read as follows: non-zero values in the raster map correspond to non-NULL; -zero values correspond to NULL. When MASK exists, masked cells are set -to null. +zero values correspond to NULL. When raster mask is active, masked cells +are set to null. \subsection Floating_point_and_type_independent_functions Floating-point and type-independent functions - Rast_maskfd() -Test for current maskreturns file descriptor number if MASK is in use -and -1 if no MASK is in use. +Test for current mask returns a file descriptor number if mask is in use +and -1 if no mask is in use. - Rast_map_is_fp() diff --git a/lib/raster/set_window.c b/lib/raster/set_window.c index 3c02d11e7d9..df6b3edf845 100644 --- a/lib/raster/set_window.c +++ b/lib/raster/set_window.c @@ -118,8 +118,8 @@ static void update_window_mappings(void) window = &twindow; */ - /* except for MASK, cell files open for read must have same projection - * and zone as new window + /* except for mask raster, cell files open for read must have same + * projection and zone as new window */ maskfd = R__.auto_mask > 0 ? R__.mask_fd : -1; for (i = 0; i < R__.fileinfo_count; i++) { diff --git a/python/grass/script/__init__.py b/python/grass/script/__init__.py index a5dfbedd239..2173ca5543f 100644 --- a/python/grass/script/__init__.py +++ b/python/grass/script/__init__.py @@ -68,7 +68,14 @@ db_table_in_vector, ) from .imagery import group_to_dict -from .raster import mapcalc, mapcalc_start, raster_history, raster_info, raster_what +from .raster import ( + mapcalc, + mapcalc_start, + raster_history, + raster_info, + raster_what, + MaskManager, +) from .raster3d import mapcalc3d, raster3d_info from .utils import ( KeyValue, @@ -108,6 +115,7 @@ __all__ = [ "PIPE", "KeyValue", + "MaskManager", "Popen", "append_node_pid", "append_random", diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index 73c555a67f0..b74362d5f6a 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -18,12 +18,13 @@ .. sectionauthor:: Martin Landa """ +from __future__ import annotations + import os import string import time from pathlib import Path - from .core import ( gisenv, find_file, @@ -36,7 +37,14 @@ fatal, ) from grass.exceptions import CalledModuleError -from .utils import encode, float_or_dms, parse_key_val, try_remove +from .utils import ( + encode, + float_or_dms, + parse_key_val, + try_remove, + append_node_pid, + append_uuid, +) def raster_history(map, overwrite=False, env=None): @@ -261,3 +269,139 @@ def raster_what(map, coord, env=None, localized=False): data.append(tmp_dict) return data + + +class MaskManager: + """Context manager for setting and managing 2D raster mask. + + The context manager makes it possible to have custom mask for the current process. + In the following example, we set the mask using _r.mask_ which creates a new + raster which represents the mask. The mask is deactivated at the end of the + context by the context manager and the raster is removed. + + >>> with gs.MaskManager(): + ... gs.run_command("r.mask", raster="state_boundary") + ... gs.parse_command("r.univar", map="elevation", format="json") + + The _mask_name_ can be a name of an existing raster map and in that case, + that raster map is used directly as is. If the raster map does not exist, + the name will be used for the mask once it is created (with _r.mask_). + + The following example uses an existing raster map directly as the mask. + The mask is disabled at the end of the context, but the raster map is not + removed. + + >>> with gs.MaskManager(mask_name="state_boundary"): + ... gs.parse_command("r.univar", map="elevation", format="json") + + Note the difference between using the name of an existing raster map directly + and using *r.mask* to create a new mask. Both zeros and NULL values are used + to represent mask resulting in NULL cells, while *r.mask* + by default sets the mask in the way that only NULL values in the original raster + result in NULL cells. + + If _mask_name_ is not provided, it generates a unique name using node (computer) + name, PID (current process ID), and unique ID (UUID). + In this case, the raster map representing the mask is removed if it exists at the + end of the context. + Optionally, the context manager can remove the raster map at the end of the context + when _remove_ is set to `True`. + The defaults for the removal of a mask raster are set to align with the two main use + cases which is creating the mask within the context and using an existing raster as + a mask. + + Name of the raster mask is available as the _mask_name_ attribute and can be used to + directly create a mask (without the need to use *r.mask*). The following example + uses the attribute to create a mask directly by name. This is equivalent to the + basic case where a raster named `MASK` is created directly by the user in an + interactive session. + + >>> with gs.MaskManager() as manager: + ... gs.run_command( + "r.mapcalc", expression=f"{manager.mask_name} = row() < col()" + ) + ... gs.run_command( + "r.mapcalc", expression=f"masked_elevation = elevation" + ) + + In the background, this class manages the `GRASS_MASK` environment variable. + It modifies the current system environment or the one provided. It does not + create a copy internally. However, the modified environment is available as + the _env_ attribute for convenience and consistency with other managers + which provide this attribute. + + The following code creates a copy of the global environment and lets the manager + modify it. The copy is then available as the _env_ attribute. + + >>> with gs.MaskManager(env=os.environ.copy()) as manager: + ... gs.run_command( + ... "r.mapcalc", + ... expression=f"{manager.mask_name} = row() < col()", + ... env=manager.env + ... ) + ... gs.run_command( + ... "r.mapcalc", expression=f"masked_elevation = elevation", env=manager.env + ... ) + """ + + def __init__( + self, + mask_name: str | None = None, + env: dict[str, str] | None = None, + remove: bool | None = None, + ): + """ + Initializes the MaskManager. + + :param mask_name: Name of the raster mask. Generated if not provided. + :param env: Environment to use. Defaults to modifying os.environ. + :param remove: If True, the raster mask will be removed when the context exits. + Defaults to True if the mask name is generated, + and False if a mask name is provided. + """ + self.env = env if env is not None else os.environ + self._original_value = None + + if mask_name is None: + self.mask_name = append_uuid(append_node_pid("mask")) + self._remove = True if remove is None else remove + else: + self.mask_name = mask_name + self._remove = False if remove is None else remove + + def __enter__(self): + """Set mask in the given environment. + + Sets the `GRASS_MASK` environment variable to the provided or + generated mask name. + + :return: Returns the MaskManager instance. + """ + self._original_value = self.env.get("GRASS_MASK") + self.env["GRASS_MASK"] = self.mask_name + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Restore the previous mask state. + + Restores the original value of `GRASS_MASK` and optionally removes + the raster mask. + + :param exc_type: Exception type, if any. + :param exc_val: Exception value, if any. + :param exc_tb: Traceback, if any. + """ + if self._original_value is not None: + self.env["GRASS_MASK"] = self._original_value + else: + self.env.pop("GRASS_MASK", None) + + if self._remove: + run_command( + "g.remove", + type="raster", + name=self.mask_name, + flags="f", + env=self.env, + quiet=True, + ) diff --git a/python/grass/script/tests/conftest.py b/python/grass/script/tests/conftest.py index ce666fbdeef..d31796562a6 100644 --- a/python/grass/script/tests/conftest.py +++ b/python/grass/script/tests/conftest.py @@ -1,7 +1,11 @@ """Fixtures for grass.script""" +import os + import pytest +import grass.script as gs + @pytest.fixture def mock_no_session(monkeypatch): @@ -18,3 +22,27 @@ def test_session_handling(): """ monkeypatch.delenv("GISRC", raising=False) monkeypatch.delenv("GISBASE", raising=False) + + +@pytest.fixture +def empty_session(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: + yield session + + +@pytest.fixture +def session_2x2(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("ones = 1", env=session.env) + gs.mapcalc( + "nulls_and_one_1_1 = if(row() == 1 && col() == 1, 1, null())", + env=session.env, + ) + yield session diff --git a/python/grass/script/tests/grass_script_raster_mask_test.py b/python/grass/script/tests/grass_script_raster_mask_test.py new file mode 100644 index 00000000000..125dda173e0 --- /dev/null +++ b/python/grass/script/tests/grass_script_raster_mask_test.py @@ -0,0 +1,225 @@ +import grass.script as gs + +DEFAULT_MASK_NAME = "MASK" + + +def raster_exists(name, env=None): + return bool(gs.find_file(name, element="raster", env=env)["name"]) + + +def raster_sum(name, env=None): + return gs.parse_command("r.univar", map="ones", env=env, format="json")[0]["sum"] + + +def test_mask_manager_no_operation(session_2x2): + """Test MaskManager not doing anything.""" + assert "GRASS_MASK" not in session_2x2.env + + with gs.MaskManager(env=session_2x2.env) as manager: + # Inside context: mask name is generated, GRASS_MASK is set + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == manager.mask_name + assert not raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(manager.mask_name) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + assert "GRASS_MASK" not in session_2x2.env + assert not raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_generated_name_remove_default_r_mask(session_2x2): + """Test MaskManager with generated name and default remove=True.""" + assert "GRASS_MASK" not in session_2x2.env + + with gs.MaskManager(env=session_2x2.env) as manager: + # Inside context: mask name is generated, GRASS_MASK is set + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == manager.mask_name + gs.run_command("r.mask", raster="nulls_and_one_1_1", env=session_2x2.env) + assert raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(manager.mask_name) + assert status["present"] + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should be removed + assert "GRASS_MASK" not in session_2x2.env + assert not raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_generated_name_remove_true_r_mask(session_2x2): + """Test MaskManager with generated name and default remove=True.""" + assert "GRASS_MASK" not in session_2x2.env + + with gs.MaskManager(env=session_2x2.env, remove=True) as manager: + # Inside context: mask name is generated, GRASS_MASK is set + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == manager.mask_name + gs.run_command("r.mask", raster="nulls_and_one_1_1", env=session_2x2.env) + assert raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(manager.mask_name) + assert status["present"] + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should be removed + assert "GRASS_MASK" not in session_2x2.env + assert not raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_generated_name_remove_false_r_mask(session_2x2): + """Test MaskManager with generated name and remove=False.""" + assert "GRASS_MASK" not in session_2x2.env + + with gs.MaskManager(env=session_2x2.env, remove=False) as manager: + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == manager.mask_name + gs.run_command("r.mask", raster="nulls_and_one_1_1", env=session_2x2.env) + assert raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(manager.mask_name) + assert status["present"] + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should not be removed + assert "GRASS_MASK" not in session_2x2.env + assert raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_generated_name_remove_true_g_copy(session_2x2): + """Test MaskManager with generated name and default remove=True and g.copy mask.""" + assert "GRASS_MASK" not in session_2x2.env + + with gs.MaskManager(env=session_2x2.env) as manager: + # Inside context: mask name is generated, GRASS_MASK is set + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == manager.mask_name + gs.run_command( + "g.copy", + raster=("nulls_and_one_1_1", manager.mask_name), + env=session_2x2.env, + ) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(manager.mask_name) + assert status["present"] + assert raster_exists(manager.mask_name, env=session_2x2.env) + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should be removed + assert "GRASS_MASK" not in session_2x2.env + assert not raster_exists(manager.mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_provided_name_remove_default(session_2x2): + """Test MaskManager with provided name and default remove=False.""" + mask_name = "nulls_and_one_1_1" + + with gs.MaskManager(mask_name=mask_name, env=session_2x2.env, remove=None): + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == mask_name + assert raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(mask_name) + assert status["present"] + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should not be removed + assert "GRASS_MASK" not in session_2x2.env + assert raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_provided_classic_name_remove_true(session_2x2): + """Test MaskManager with provided name and remove=True.""" + mask_name = DEFAULT_MASK_NAME + + with gs.MaskManager(mask_name=mask_name, env=session_2x2.env, remove=True): + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == mask_name + gs.run_command("r.mask", raster="nulls_and_one_1_1", env=session_2x2.env) + assert raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(mask_name) + assert status["present"] + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should not be removed + assert "GRASS_MASK" not in session_2x2.env + assert not raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_provided_name_remove_true(session_2x2): + """Test MaskManager with provided name and remove=True.""" + mask_name = "nulls_and_one_1_1" + + with gs.MaskManager( + mask_name=mask_name, env=session_2x2.env, remove=True + ) as manager: + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == mask_name + assert raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(manager.mask_name) + assert status["present"] + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should be removed + assert "GRASS_MASK" not in session_2x2.env + assert not raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 + + +def test_mask_manager_provided_name_remove_false(session_2x2): + """Test MaskManager with provided name and remove=False.""" + mask_name = "nulls_and_one_1_1" + + with gs.MaskManager( + mask_name=mask_name, env=session_2x2.env, remove=False + ) as manager: + assert "GRASS_MASK" in session_2x2.env + assert session_2x2.env["GRASS_MASK"] == mask_name + assert raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(manager.mask_name) + assert status["present"] + assert raster_sum("ones", env=session_2x2.env) == 1 + + # After context: GRASS_MASK unset, mask should not be removed + assert "GRASS_MASK" not in session_2x2.env + assert raster_exists(mask_name, env=session_2x2.env) + status = gs.parse_command("r.mask.status", format="json", env=session_2x2.env) + assert status["name"].startswith(DEFAULT_MASK_NAME) + assert not status["present"] + assert raster_sum("ones", env=session_2x2.env) == 4 diff --git a/raster/r.mapcalc/testsuite/const_map_test.sh b/raster/r.mapcalc/testsuite/const_map_test.sh index 2f66d5085cc..bf138f4256b 100755 --- a/raster/r.mapcalc/testsuite/const_map_test.sh +++ b/raster/r.mapcalc/testsuite/const_map_test.sh @@ -46,26 +46,17 @@ cleanup() g.remove -f type=raster name=$TMPNAME > /dev/null } -# check if a MASK is already present: +# Create our own mask. MASKTMP=mask.$TMPNAME -USERMASK=usermask_${MASKTMP} -if test -f $MAPSET_PATH/cell/MASK -then - echo "A user raster mask (MASK) is present. Saving it..." - g.rename raster=MASK,$USERMASK > /dev/null -fi +export GRASS_MASK=$MASKTMP finalcleanup() { echo "Restoring user region" g.region region=$TMPNAME g.remove -f type=region name=$TMPNAME > /dev/null - #restore user mask if present: - if test -f $MAPSET_PATH/cell/$USERMASK ; then - echo "Restoring user MASK" - g.remove -f type=raster name=MASK > /dev/null - g.rename raster=$USERMASK,MASK > /dev/null - fi + # Remove our mask if present. + g.remove -f type=raster name=$MASKTMP > /dev/null } check_exit_status() @@ -106,15 +97,10 @@ compare_result() fi } -#check if a MASK is already present: +# Deactive the current mask, by using our own mask name, +# but not creating any mask. MASKTMP=mask.$TMPNAME -USERMASK=usermask_${MASKTMP} -if test -f $MAPSET_PATH/cell/MASK -then - echo "A user raster mask (MASK) is present. Saving it..." - g.rename raster=MASK,$USERMASK > /dev/null - check_exit_status $? -fi +export GRASS_MASK=$MASKTMP echo "Saving current & setting test region." g.region save=$TMPNAME diff --git a/raster/r.mask.status/tests/r_mask_status_test.py b/raster/r.mask.status/tests/r_mask_status_test.py index a5d406ad581..e9856ee08cf 100644 --- a/raster/r.mask.status/tests/r_mask_status_test.py +++ b/raster/r.mask.status/tests/r_mask_status_test.py @@ -10,6 +10,9 @@ import grass.script as gs +DEFAULT_MASK_NAME = "MASK" + + def test_json_no_mask(session_no_data): """Check JSON format for no mask""" session = session_no_data @@ -17,7 +20,7 @@ def test_json_no_mask(session_no_data): assert "present" in data assert "name" in data assert data["name"], "Mask name needs to be always set" - assert data["name"] == "MASK@PERMANENT", "Default mask name and current mapset" + assert data["name"] == f"{DEFAULT_MASK_NAME}@PERMANENT" assert "is_reclass_of" in data assert data["present"] is False assert not data["is_reclass_of"] @@ -29,29 +32,31 @@ def test_json_with_r_mask(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["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@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 data["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@PERMANENT" 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) + gs.run_command("g.copy", raster=["a", DEFAULT_MASK_NAME], env=session.env) data = gs.parse_command("r.mask.status", format="json", env=session.env) assert data["present"] is True - assert data["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@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) + gs.run_command( + "g.remove", type="raster", name=DEFAULT_MASK_NAME, flags="f", env=session.env + ) data = gs.parse_command("r.mask.status", format="json", env=session.env) assert data["present"] is False - assert data["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@PERMANENT" assert not data["is_reclass_of"] @@ -61,13 +66,13 @@ def test_shell(session_with_data): gs.run_command("r.mask", raster="a", env=session.env) data = gs.parse_command("r.mask.status", format="shell", env=session.env) assert int(data["present"]) - assert data["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@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="shell", env=session.env) assert not int(data["present"]) - assert data["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@PERMANENT" assert not data["is_reclass_of"] @@ -79,14 +84,14 @@ def test_yaml(session_with_data): text = gs.read_command("r.mask.status", format="yaml", env=session.env) data = yaml.safe_load(text) assert data["present"] is True - assert data["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@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 data["name"] == "MASK@PERMANENT" + assert data["name"] == f"{DEFAULT_MASK_NAME}@PERMANENT" assert not data["is_reclass_of"] @@ -96,13 +101,13 @@ def test_plain(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 f"{DEFAULT_MASK_NAME}@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 - assert "MASK@PERMANENT" in text + assert f"{DEFAULT_MASK_NAME}@PERMANENT" in text assert "a@PERMANENT" not in text diff --git a/raster/rasterintro.html b/raster/rasterintro.html index 51770eceb00..e1df2d22e11 100644 --- a/raster/rasterintro.html +++ b/raster/rasterintro.html @@ -21,9 +21,13 @@

Raster maps in general

of the current computational region.
  • Raster input maps are automatically cropped/padded and rescaled (using nearest-neighbour resampling) to match the current region.
  • -
  • Raster input maps are automatically masked if a raster map named - MASK exists. The MASK is only applied when reading maps - from the disk.
  • +
  • Processing NULL (no data) values produces NULL values.
  • +
  • Input raster maps are automatically masked if a raster mask is active, + The mask is managed by the r.mask tool, and + it is represented by a raster map called MASK by default. + Unless specified otherwise, the raster mask is only applied + when reading raster maps which typically results in NULL values + in the output for areas outside of the mask.
  • There are a few exceptions to this: @@ -172,14 +176,20 @@

    Resampling methods and interpolation methods

    Otherwise, for interpolation of scattered data, use the v.surf.* set of modules. -

    Raster MASKs

    +

    Raster masks

    If a raster map named "MASK" exists, most GRASS raster modules will operate only on data falling inside the masked area, and treat any data falling outside of the mask as if its value were NULL. The mask is only applied when reading an existing GRASS raster map, for example when used in a module as an input map. +While the mask raster map can be managed directly, +the r.mask tool is a more convenient way to create +and manage masks.

    -The mask is read as an integer map. If MASK is actually a +Alternatively, GRASS_MASK environment variable can be used to specify +the raster map which will be used as a mask. +

    +The mask is read as an integer map. If the mask raster is actually a floating-point map, the values will be converted to integers using the map's quantisation rules (this defaults to round-to-nearest, but can be changed with r.quant). diff --git a/scripts/r.fillnulls/r.fillnulls.py b/scripts/r.fillnulls/r.fillnulls.py index 23519b88122..c5e6211d5eb 100755 --- a/scripts/r.fillnulls/r.fillnulls.py +++ b/scripts/r.fillnulls/r.fillnulls.py @@ -12,9 +12,9 @@ # Per hole filling with RST by Maris Nartiss # Speedup for per hole filling with RST by Stefan Blumentrath # PURPOSE: fills NULL (no data areas) in raster maps -# The script respects a user mask (MASK) if present. +# The script respects a user mask if present. # -# COPYRIGHT: (C) 2001-2018 by the GRASS Development Team +# COPYRIGHT: (C) 2001-2025 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 @@ -117,7 +117,7 @@ def cleanup(): - # delete internal mask and any TMP files: + # delete internal any temporary files: if len(tmp_vmaps) > 0: gs.run_command( "g.remove", quiet=True, flags="fb", type="vector", name=tmp_vmaps @@ -126,11 +126,6 @@ def cleanup(): gs.run_command( "g.remove", quiet=True, flags="fb", type="raster", name=tmp_rmaps ) - if usermask and mapset: - if gs.find_file(usermask, mapset=mapset)["file"]: - gs.run_command( - "g.rename", quiet=True, raster=(usermask, "MASK"), overwrite=True - ) def main(): @@ -159,13 +154,11 @@ def main(): # save original region reg_org = gs.region() - # check if a MASK is already present - # and remove it to not interfere with NULL lookup part - # as we don't fill MASKed parts! - if gs.find_file("MASK", mapset=mapset)["file"]: - usermask = "usermask_mask." + unique - gs.message(_("A user raster mask (MASK) is present. Saving it...")) - gs.run_command("g.rename", quiet=quiet, raster=("MASK", usermask)) + # Check if a raster mask is present. + # We need to handle the mask in a special way, so we don't fill the masked parts. + mask_status = gs.parse_command("r.mask.status", format="json") + if mask_status["present"]: + usermask = mask_status["name"] # check if method is rst to use v.surf.rst if method == "rst": @@ -184,16 +177,18 @@ def main(): # creating binary (0/1) map if usermask: + # Disable masking to not interfere with NULL lookup part. gs.message(_("Skipping masked raster parts")) - gs.mapcalc( - ( - '$tmp1 = if(isnull("$input") && !($mask == 0 || isnull($mask)),1,' - "null())" - ), - tmp1=prefix + "nulls", - input=input, - mask=usermask, - ) + with gs.MaskManager(): + gs.mapcalc( + ( + '$tmp1 = if(isnull("$input") && !($mask == 0 || isnull($mask)),' + "1,null())" + ), + tmp1=prefix + "nulls", + input=input, + mask=usermask, + ) else: gs.mapcalc( '$tmp1 = if(isnull("$input"),1,null())', @@ -202,16 +197,6 @@ def main(): ) tmp_rmaps.append(prefix + "nulls") - # restoring user's mask, if present - # to ignore MASKed original values - if usermask: - gs.message(_("Restoring user mask (MASK)...")) - try: - gs.run_command("g.rename", quiet=quiet, raster=(usermask, "MASK")) - except CalledModuleError: - gs.warning(_("Failed to restore user MASK!")) - usermask = None - # grow identified holes by X pixels gs.message(_("Growing NULL areas")) tmp_rmaps.append(prefix + "grown") @@ -590,21 +575,22 @@ def main(): new_env["LC_ALL"] = "C" if usermask: try: - p = gs.core.start_command( - "r.resamp.bspline", - input=input, - mask=usermask, - output=prefix + "filled", - method=method, - ew_step=3 * reg["ewres"], - ns_step=3 * reg["nsres"], - lambda_=lambda_, - memory=memory, - flags="n", - stderr=subprocess.PIPE, - env=new_env, - ) - stderr = gs.decode(p.communicate()[1]) + with gs.MaskManager(): + p = gs.core.start_command( + "r.resamp.bspline", + input=input, + mask=usermask, + output=prefix + "filled", + method=method, + ew_step=3 * reg["ewres"], + ns_step=3 * reg["nsres"], + lambda_=lambda_, + memory=memory, + flags="n", + stderr=subprocess.PIPE, + env=new_env, + ) + stderr = gs.decode(p.communicate()[1]) if "No NULL cells found" in stderr: gs.run_command( "g.copy", raster="%s,%sfilled" % (input, prefix), overwrite=True @@ -656,15 +642,6 @@ def main(): % stderr ) - # restoring user's mask, if present: - if usermask: - gs.message(_("Restoring user mask (MASK)...")) - try: - gs.run_command("g.rename", quiet=quiet, raster=(usermask, "MASK")) - except CalledModuleError: - gs.warning(_("Failed to restore user MASK!")) - usermask = None - # set region to original extents, align to input gs.run_command( "g.region", diff --git a/scripts/r.in.wms/wms_base.py b/scripts/r.in.wms/wms_base.py index 2d959289b23..0f44b3e7ea9 100644 --- a/scripts/r.in.wms/wms_base.py +++ b/scripts/r.in.wms/wms_base.py @@ -502,19 +502,14 @@ def _tempfile(self): class GRASSImporter: def __init__(self, opt_output, cleanup_bands): - self.cleanup_mask = False self.cleanup_bands = cleanup_bands # output map name self.opt_output = opt_output - # suffix for existing mask (during overriding will be saved - # into raster named:self.opt_output + this suffix) - self.original_mask_suffix = "_temp_MASK" - # check names of temporary rasters, which module may create maps = [] - for suffix in (".red", ".green", ".blue", ".alpha", self.original_mask_suffix): + for suffix in (".red", ".green", ".blue", ".alpha"): rast = self.opt_output + suffix if gs.find_file(rast, element="cell", mapset=".")["file"]: maps.append(rast) @@ -529,24 +524,6 @@ def __init__(self, opt_output, cleanup_bands): ) def __del__(self): - # removes temporary mask, used for import transparent or warped temp_map - if self.cleanup_mask: - # clear temporary mask, which was set by module - try: - gs.run_command("r.mask", quiet=True, flags="r") - except CalledModuleError: - gs.fatal(_("%s failed") % "r.mask") - - # restore original mask, if exists - if gs.find_file( - self.opt_output + self.original_mask_suffix, element="cell", mapset="." - )["name"]: - try: - mask_copy = self.opt_output + self.original_mask_suffix - gs.run_command("g.copy", quiet=True, raster=mask_copy + ",MASK") - except CalledModuleError: - gs.fatal(_("%s failed") % "g.copy") - # remove temporary created rasters maps = [] rast = self.opt_output + ".alpha" @@ -554,7 +531,7 @@ def __del__(self): maps.append(rast) if self.cleanup_bands: - for suffix in (".red", ".green", ".blue", self.original_mask_suffix): + for suffix in (".red", ".green", ".blue"): rast = self.opt_output + suffix if gs.find_file(rast, element="cell", mapset=".")["file"]: maps.append(rast) @@ -615,17 +592,12 @@ def ImportMapIntoGRASS(self, raster): # mask created from alpha layer, which describes real extend # of warped layer (may not be a rectangle), also mask contains # transparent parts of raster - if gs.find_file(self.opt_output + ".alpha", element="cell", mapset=".")["name"]: - # saving current mask (if exists) into temp raster - if gs.find_file("MASK", element="cell", mapset=".")["name"]: - try: - mask_copy = self.opt_output + self.original_mask_suffix - gs.run_command("g.copy", quiet=True, raster="MASK," + mask_copy) - except CalledModuleError: - gs.fatal(_("%s failed") % "g.copy") + with gs.MaskManager(): + self._post_process() - # info for destructor - self.cleanup_mask = True + def _post_process(self): + """Embed nulls from alpha and create RGB composite""" + if gs.find_file(self.opt_output + ".alpha", element="cell", mapset=".")["name"]: try: gs.run_command( "r.mask", @@ -639,7 +611,7 @@ def ImportMapIntoGRASS(self, raster): gs.fatal(_("%s failed") % "r.mask") if not self.cleanup_bands: - # use the MASK to set NULL values + # Use the alpha-derived mask to set NULL values. for suffix in (".red", ".green", ".blue"): rast = self.opt_output + suffix if gs.find_file(rast, element="cell", mapset=".")["file"]: diff --git a/scripts/r.mask/r.mask.html b/scripts/r.mask/r.mask.html index 312de990c47..dd3c918488e 100644 --- a/scripts/r.mask/r.mask.html +++ b/scripts/r.mask/r.mask.html @@ -1,72 +1,95 @@

    DESCRIPTION

    -r.mask facilitates the creation of a raster "MASK" map to +r.mask facilitates the creation and management of a raster mask to control raster operations. +While the computational region specifies the extent (rectangular bounding box) +and resolution, the mask specifies the area that should be considered for +the operations and area which should be ignored. +The mask is represented as a raster map called MASK by default.

    -The MASK is applied when reading an existing GRASS raster map, -for example when used as an input map in a module. The MASK will block out +The mask is applied when reading an existing GRASS raster map, +for example when used as an input map in a module. The mask will block out certain areas of a raster map from analysis and/or display, by "hiding" them from sight of other GRASS modules. Data falling within the boundaries of the -MASK can be modified and operated upon by other GRASS raster modules; data -falling outside the MASK is treated as if it were NULL. +mask can be modified and operated upon by other GRASS raster modules; data +falling outside the mask is treated as if it were NULL.

    By default, r.mask converts any non-NULL value in the input map, -including zero, to 1. All these areas will be part of the MASK (see the notes +including zero, to 1. All these areas will be part of the mask (see the notes for more details). To only convert specific values (or range of values) to 1 and the rest to NULL, use the maskcats parameter.

    -Because the MASK created with r.mask is actually only a reclass map -named "MASK", it can be copied, renamed, removed, and used in analyses, just -like other GRASS raster map layers. +Because the mask raster map created with r.mask is actually only +a reclass map named MASK by default, it can be copied, renamed, +removed, and used in analyses, just like other GRASS raster maps.

    -The user should be aware that a MASK remains in place until a user renames it -to something other than "MASK", or removes it. To remove a mask and restore +The user should be aware that a mask remains in place until it is removed +or renamed. To remove a mask and restore raster operations to normal (i.e., all cells of the current region), remove the -MASK by setting the -r remove MASK flag (r.mask -r). +mask by setting the -r flag (r.mask -r). Alternatively, a mask can be removed using g.remove or by renaming it to any other name with g.rename. +

    +The GRASS_MASK environment variable can be used to specify +the raster map which will be used as a mask. If the environment variable is +not defined, the name MASK is used instead.

    NOTES

    The above method for specifying a "mask" may seem counterintuitive. Areas -inside the MASK are not hidden; areas outside the MASK will be ignored until -the MASK file is removed. +inside the mask are not hidden; areas outside the mask will be ignored until +the mask is removed.

    r.mask uses r.reclass to create a reclassification of an -existing raster map and name it MASK. A reclass map takes up less +existing raster map and names it MASK by default. +A reclass map takes up less space, but is affected by any changes to the underlying map from which it was created. The user can select category values from the input raster to use in the -MASK with the maskcats parameter; if r.mask is run from the +mask with the maskcats parameter; if r.mask is run from the command line, the category values listed in maskcats must be quoted (see example below). Note that the maskcats can only be used if the input map is an integer map. -

    Different ways to create a MASK

    +

    Different ways to create a mask

    + +Mask can be created using r.mask or by creating a mask raster map +directly. -The r.mask function creates a MASK with values 1 and NULL. But note -that a MASK can also be created using other functions that have a raster as -output, by naming the output raster 'MASK'. Such layers could have other -values than 1 and NULL. The user should therefore be aware that grid cells -in the MASK map containing NULL or 0 will replace data with -NULL, while cells containing other values will allow data to pass through -unaltered. This means that:

    -If a binary map with [0,1] values is used as input in r.mask, all -raster cells with 0 and 1 will be part of the MASK. This is because -r.mask converts all non-NULL cells to 1. +The r.mask tool creates a mask with values 1 and NULL. +By default, r.mask converts all non-NULL cells to 1. +If a raster with ones (1) and NULLs values is used with r.mask, all +raster cells with value 1 will be included in the computation, +while those with NULL will be masked out.

    -r.mapcalc -s "map1 = round(rand(0,1))"
    +r.mapcalc -s "map1 = if(row() < col(), 1, null())"
     r.mask raster=map1
     
    -On the other hand, if a binary map is used as an input in g.copy to create a MASK, -only the raster cells with value 1 will be part of the MASK. +If a binary map with zeros and ones as values is used with r.mask, +and both NULLs and zeros should be masked out, the maskcats parameter +can be used to specify the values that should be masked out: + +
    +r.mapcalc -s "map2 = if(row() < col(), 1, 0)"
    +r.mask raster=map2 maskcats="0"
    +
    + +

    +Mask can also be created directly using any tools that have a raster as +output, by naming the output raster MASK (or whatever the +GRASS_MASK environment variable is set to). +Both NULLs and zeros will be masked out in the raster mask. +This allows for creation of a simple binary raster with only ones and zeros +where cells with zeros in the mask raster are excluded from the computation +(behaving as if they were NULL). +A raster with zeros and ones can be created and used directly as a mask +by naming the output raster MASK, e.g., using raster algebra:

    -r.mapcalc -s "map2 = round(rand(0,1))"
    -g.copy raster=map2,MASK
    +r.mapcalc -s "MASK = if(row() < col(), 1, 0)"
     

    Handling of floating-point maps

    @@ -74,8 +97,8 @@

    Handling of floating-point maps

    r.mask treats floating-point maps the same as integer maps (except that floating maps are not allowed in combination with the maskcats parameter); all non-NULL values of the input raster map are converted to 1 and -are thus part of the MASK. In the example below, all raster cells are part of -the MASK, i.e., nothing is blocked out from analysis and/or display. +are thus part of the mask. In the example below, all raster cells are part of +the mask, i.e., nothing is blocked out from analysis and/or display.
     r.mapcalc -s "map3 = rand(0.0,1.0)"
    @@ -83,7 +106,7 @@ 

    Handling of floating-point maps

    However, when using another method than r.mask to create a mask, -the user should be aware that the MASK is read as an integer map. If MASK is +the user should be aware that the mask is read as an integer map. If mask is a floating-point map, the values will be converted to integers using the map's quantisation rules (this defaults to round-to-nearest, but can be changed with r.quant). @@ -94,7 +117,7 @@

    Handling of floating-point maps

    In the example above, raster cells with a rounded value of 1 are part of -the MASK, while raster cells with a rounded value of 0 are converted to NULL +the mask, while raster cells with a rounded value of 0 are converted to NULL and consequently blocked out from analysis and/or display.

    EXAMPLES

    @@ -106,13 +129,13 @@

    EXAMPLES

     # set computation region to lakes raster map
     g.region raster=lakes -p
    -# use lakes as MASK
    +# use lakes as mask
     r.mask raster=lakes
     # get statistics for elevation pixels of lakes:
     r.univar elevation
     
    -Remove the raster mask ("MASK" map) with the -r flag: +Remove the raster mask with the -r flag:
     r.mask -r
     
    diff --git a/scripts/r.mask/r.mask.py b/scripts/r.mask/r.mask.py index 882fd8d1e75..b30fd94965c 100755 --- a/scripts/r.mask/r.mask.py +++ b/scripts/r.mask/r.mask.py @@ -7,7 +7,7 @@ # Markus Neteler # Converted to Python by Glynn Clements # Markus Metz -# PURPOSE: Facilitates creation of raster MASK +# PURPOSE: Facilitates creation of 2D raster mask # COPYRIGHT: (C) 2005-2013 by the GRASS Development Team # # This program is free software under the GNU General Public @@ -17,7 +17,7 @@ ############################################################################# # %module -# % description: Creates a MASK for limiting raster operation. +# % description: Creates a raster mask for limiting raster operation. # % keyword: raster # % keyword: mask # % keyword: null data @@ -98,36 +98,52 @@ def main(): gs.fatal(_("Either parameter or parameter is required")) mapset = gs.gisenv()["MAPSET"] - exists = bool(gs.find_file("MASK", element="cell", mapset=mapset)["file"]) + mask_status = gs.parse_command("r.mask.status", format="json") + exists = mask_status["present"] + mask_full_name = mask_status["name"] + + # Does the name point to the current mapset? + name, mask_mapset = mask_full_name.split("@", maxsplit=1) + if mask_mapset != mapset: + gs.fatal( + _( + "r.mask can only operate on raster mask in the current mapset " + "({current_mapset}), but mask name is set to {full_name}" + ).format(current_mapset=mapset, full_name=mask_full_name) + ) if remove: # -> remove if exists: if sys.platform == "win32": gs.run_command( - "g.remove", flags="if", quiet=True, type="raster", name="MASK" + "g.remove", flags="if", quiet=True, type="raster", name=name ) else: gs.run_command( - "g.remove", flags="f", quiet=True, type="raster", name="MASK" + "g.remove", flags="f", quiet=True, type="raster", name=name ) - gs.message(_("Raster MASK removed")) + gs.message(_("Raster mask removed")) else: - gs.fatal(_("No existing MASK to remove")) + gs.fatal( + _("No existing mask to remove (no raster named {name})").format( + name=name + ) + ) else: # -> create if exists: if not gs.overwrite(): gs.fatal( _( - "MASK already found in current mapset. Delete first or " - "overwrite." - ) + "Raster mask is already present in the current mapset. " + "Use overwrite or delete raster named {name}." + ).format(name=name) ) else: - gs.warning(_("MASK already exists and will be overwritten")) + gs.warning(_("Raster mask already exists and will be overwritten")) gs.run_command( - "g.remove", flags="f", quiet=True, type="raster", name="MASK" + "g.remove", flags="f", quiet=True, type="raster", name=name ) if raster: @@ -146,7 +162,7 @@ def main(): ) p = gs.feed_command( - "r.reclass", input=raster, output="MASK", overwrite=True, rules="-" + "r.reclass", input=raster, output=name, overwrite=True, rules="-" ) res = "%s = 1" % maskcats p.stdin.write(encode(res)) @@ -167,7 +183,7 @@ def main(): gs.warning( _( "No area found in vector map <%s>. " - "Creating a convex hull for MASK." + "Creating a convex hull to create a raster mask." ) % vector_name ) @@ -201,7 +217,7 @@ def main(): "v.to.rast", input=to_rast_input, layer=layer, - output="MASK", + output=name, use="value", value="1", type="area", @@ -213,19 +229,19 @@ def main(): if invert: global tmp tmp = "r_mask_%d" % os.getpid() - gs.run_command("g.rename", raster=("MASK", tmp), quiet=True) - gs.message(_("Creating inverted raster MASK...")) - gs.mapcalc("MASK = if(isnull($tmp), 1, null())", tmp=tmp) - gs.verbose(_("Inverted raster MASK created")) + gs.run_command("g.rename", raster=(name, tmp), quiet=True) + gs.message(_("Creating inverted raster mask...")) + gs.mapcalc("$mask = if(isnull($tmp), 1, null())", mask=name, tmp=tmp) + gs.verbose(_("Inverted raster mask created")) else: - gs.verbose(_("Raster MASK created")) + gs.verbose(_("Raster mask created")) gs.message( _( "All subsequent raster operations will be limited to " - "the MASK area. Removing or renaming raster map named " - "'MASK' will restore raster operations to normal." - ) + "the raster mask area. Removing the mask (with r.mask -r) or renaming " + "a raster map named '{name}' will restore raster operations to normal." + ).format(name=name) ) diff --git a/testsuite/raster_md5test.sh b/testsuite/raster_md5test.sh index b3281d4c505..30a82677090 100755 --- a/testsuite/raster_md5test.sh +++ b/testsuite/raster_md5test.sh @@ -58,26 +58,17 @@ cleanup() g.remove -f type=raster name="$TMPNAME" > /dev/null } -# check if a MASK is already present: +# Create our own mask. MASKTMP="mask.${TMPNAME}" -USERMASK="usermask_${MASKTMP}" -if test -f "${MAPSET_PATH}/cell/MASK" -then - echo "A user raster mask (MASK) is present. Saving it..." - g.rename raster=MASK,"$USERMASK" > /dev/null -fi +export GRASS_MASK="$MASKTMP" finalcleanup() { echo "Restoring user region" g.region region="$TMPNAME" g.remove -f type=region name="$TMPNAME" > /dev/null - #restore user mask if present: - if test -f "${MAPSET_PATH}/cell/${USERMASK}" ; then - echo "Restoring user MASK" - g.remove -f type=raster name=MASK > /dev/null - g.rename raster="$USERMASK",MASK > /dev/null - fi + # Remove our mask if present. + g.remove -f type=raster name="$MASKTMP" > /dev/null } check_exit_status()