"""
+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:
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()