Просмотр исходного кода

Simplify raster band reference management (#1272)

Refactor bandref code:
* As band references are used only for raster, move functions to raster lib and model them after units and vdatum metadata
* Add function to test basic band id naming conformance
* Add ability to find out if provided band metadata file name is present
* Make filename in band reference not mandatory to accommodate any band id also without extended metadata
As file name can be determined at runtime and is used only to print out extended metadata, it makes little sense to store it. This greatly simplifies work with band references.
* Enable band reference management in raster support module
* Print band reference in r.info output
* Fix i.band to work with updated band reference code + remove test dependency on a particular location
* Adjust max length check & tests (thanks to @nilason)
Māris Nartišs 3 лет назад
Родитель
Сommit
ca1551206e

+ 0 - 4
include/grass/defs/gis.h

@@ -171,10 +171,6 @@ int G_vfaprintf(FILE *, const char *, va_list);
 int G_vsaprintf(char *, const char *, va_list);
 int G_vsnaprintf(char *, size_t, const char *, va_list);
 
-/* bands.c */
-int G__read_band_reference(FILE *, struct Key_Value **);
-int G__write_band_reference(FILE *, const char *, const char *);
-
 /* basename.c */
 char *G_basename(char *, const char *);
 size_t G_get_num_decimals(const char *);

+ 3 - 6
include/grass/defs/raster.h

@@ -35,12 +35,6 @@ int Rast__check_for_auto_masking(void);
 void Rast_suppress_masking(void);
 void Rast_unsuppress_masking(void);
 
-/* bands.c */
-int Rast_has_band_reference(const char *, const char *);
-int Rast_read_band_reference(const char *, const char *, char **, char **);
-int Rast_write_band_reference(const char *, const char *, const char *);
-int Rast_remove_band_reference(const char *);
-
 /* cats.c */
 int Rast_read_cats(const char *, const char *, struct Categories *);
 int Rast_read_vector_cats(const char *, const char *, struct Categories *);
@@ -550,8 +544,11 @@ DCELL Rast_get_d_value(const void *, RASTER_MAP_TYPE);
 /* raster_metadata.c */
 char *Rast_read_units(const char *, const char *);
 char *Rast_read_vdatum(const char *, const char *);
+char *Rast_read_bandref(const char *, const char *);
 void Rast_write_units(const char *, const char *);
 void Rast_write_vdatum(const char *, const char *);
+void Rast_write_bandref(const char *, const char *);
+int Rast_legal_bandref(const char *);
 
 /* rast_to_img_string.c */
 int Rast_map_to_img_str(char *, int, unsigned char*);

+ 0 - 78
lib/gis/band_reference.c

@@ -1,78 +0,0 @@
-/*!
- * \file lib/gis/band_reference.c
- *
- * \brief GIS Library - Band reference management (internal use only)
- *
- * (C) 2019 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 Martin Landa (with financial support by mundialis, Bonn, for openEO EU H2020 grant 776242, https://openeo.org)
- */
-
-#include <grass/gis.h>
-#include <grass/glocale.h>
-
-/*!
-   \brief Read band reference identifier from file (internal use only).
-
-  \param fd file descriptor
-  \param[out] key_val key/value pairs (filename, identifier)
-
-  \return 1 on success
-  \return -1 error - unable fetch key/value pairs
-  \return -2 error - invalid band reference
- */
-int G__read_band_reference(FILE *fd, struct Key_Value **key_val)
-{
-    const char *filename, *band_ref;
-
-    *key_val = G_fread_key_value(fd);
-    if (*key_val) {
-        G_debug(1, "No band reference detected");
-        return -1;
-    }
-
-    filename = G_find_key_value("file", *key_val);
-    band_ref = G_find_key_value("identifier", *key_val);
-    if (!filename || !band_ref) {
-        G_debug(1, "Invalid band reference identifier");
-        return -2;
-    }
-
-    G_debug(1, "Band reference <%s> (%s)", band_ref, filename);
-
-    return 1;
-}
-
-/*!
-   \brief Write band reference identifier to file (internal use only).
-
-   \param fd file descriptor
-   \param filename filename JSON reference
-   \param band_reference band reference identifier
-
-   \return 1 on success
-   \return -1 error - unable to write key/value pairs into fileo
-*/
-int G__write_band_reference(FILE *fd,
-                            const char *filename, const char *band_reference)
-{
-    struct Key_Value *key_val;
-
-    key_val = G_create_key_value();
-    G_set_key_value("file", filename, key_val);
-    G_set_key_value("identifier", band_reference, key_val);
-
-    if (G_fwrite_key_value(fd, key_val) < 0) {
-        G_debug(1, "Error writing band reference file");
-        G_free_key_value(key_val);
-        return -1;
-    }
-    G_free_key_value(key_val);
-
-    return 1;
-}
-

+ 0 - 126
lib/raster/band_reference.c

@@ -1,126 +0,0 @@
-/*!
- * \file lib/raster/band_reference.c
- *
- * \brief Raster Library - Band reference managenent
- *
- * (C) 2019 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 Martin Landa (with financial support by mundialis, Bonn, for openEO EU H2020 grant 776242, https://openeo.org)
- */
-
-#include <string.h>
-
-#include <grass/gis.h>
-#include <grass/raster.h>
-#include <grass/glocale.h>
-
-static const char *_band_file = "band_reference";
-
-/*!
-  \brief Check if band reference for raster map exists
-
-  \param name map name
-  \param mapset mapset name
-
-  \return 1 on success
-  \return 0 no band reference present
-*/
-int Rast_has_band_reference(const char *name, const char *mapset)
-{
-    if (!G_find_file2_misc("cell_misc", _band_file, name, mapset))
-	return 0;
-
-    return 1;
-}
-
-/*!
-   \brief Read raster map band reference identifier.
-
-   Note that output arguments should be freed by the caller using G_free().
-
-   \param name map name
-   \param mapset mapset name
-   \param[out] filename filename JSON reference
-   \param[out] band_reference band reference identifier
-
-  \return 1 on success
-  \return 0 band reference not found
-  \return negative on error
- */
-int Rast_read_band_reference(const char *name, const char *mapset,
-                             char **filename, char **band_reference)
-{
-    int ret;
-    FILE *fd;
-    struct Key_Value *key_val;
-
-    G_debug(1, "Reading band reference file for raster map <%s@%s>",
-            name, mapset);
-
-    if (!Rast_has_band_reference(name, mapset))
-        return 0;
-
-    fd = G_fopen_old_misc("cell_misc", _band_file, name, mapset);
-    if (!fd) {
-        G_debug(1, "Unable to read band identifier file for <%s@%s>",
-                name, mapset);
-        return -1;
-    }
-
-    ret = G__read_band_reference(fd, &key_val);
-    *filename = G_store(G_find_key_value("file", key_val));
-    *band_reference = G_store(G_find_key_value("identifier", key_val));
-
-    fclose(fd);
-    G_free_key_value(key_val);
-
-    return ret;
-}
-
-/*!
-   \brief Write raster map band reference identifier.
-
-   \param name map name
-   \param filename filename JSON reference
-   \param band_reference band reference identifier
-
-   \return 1 on success
-   \return negative on error
- */
-int Rast_write_band_reference(const char *name,
-                              const char *filename, const char *band_reference)
-{
-    int ret;
-    FILE *fd;
-
-    fd = G_fopen_new_misc("cell_misc", _band_file, name);
-    if (!fd) {
-        G_fatal_error(_("Unable to create band file for <%s>"), name);
-        return -1;
-    }
-
-    ret = G__write_band_reference(fd, filename, band_reference);
-    fclose(fd);
-
-    return ret;
-}
-
-/*!
-  \brief Remove band reference from raster map
-
-  Only band reference files in current mapset can be removed.
-
-  \param name map name
-
-  \return 0 if no file
-  \return 1 on success
-  \return -1 on error
-*/
-int Rast_remove_band_reference(const char *name)
-{
-    return G_remove_misc("cell_misc", _band_file, name);
-}

+ 95 - 12
lib/raster/raster_metadata.c

@@ -1,10 +1,11 @@
 /*!
    \file lib/raster/raster_metadata.c
 
-   \brief Raster library - Functions to read and write raster "units"
-   and "vertical datum" meta-data info
+   \brief Raster library - Functions to read and write raster "units",
+   "band reference" and "vertical datum" meta-data info
 
-   (C) 2007-2009 by Hamish Bowman, and the GRASS Development Team
+   (C) 2007-2009, 2021 by Hamish Bowman, Maris Nartiss,
+   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.
@@ -81,6 +82,86 @@ void Rast_write_vdatum(const char *name, const char *str)
     misc_write_line("vertical_datum", name, str);
 }
 
+/*!
+ * \brief Get a raster map's band reference metadata string
+ *
+ * Read the raster's band reference metadata file and put string in str
+ *
+ * \param name raster map name
+ * \param mapset mapset name
+ *
+ * \return  string representing band reference on success
+ * \return  NULL on error
+ */
+char *Rast_read_bandref(const char *name, const char *mapset)
+{
+    return misc_read_line("bandref", name, mapset);
+}
+
+/*!
+ * \brief Write a string into a raster's band reference metadata file
+ *
+ * Raster map must exist in the current mapset.
+ *
+ * It is up to the caller to validate band reference string in advance
+ * with Rast_legal_bandref().
+ *
+ * \param name raster map name
+ * \param str  string containing data to be written
+ */
+void Rast_write_bandref(const char *name, const char *str)
+{
+    misc_write_line("bandref", name, str);
+}
+
+/*!
+ * \brief Check for legal band reference
+ *
+ * Legal band identifiers must be legal GRASS file names.
+ * They are in format <shortcut>_<bandname>.
+ * Band identifiers are capped in legth to GNAME_MAX.
+ *
+ * This function will return -1 if provided band id is not considered
+ * to be valid.
+ * This function does not check if band id maps to any entry in band
+ * metadata files as not all band id's have files with extra metadata.
+ *
+ * The function prints a warning on error.
+ *
+ * \param bandref band reference to check
+ *
+ * \return 1 success
+ * \return -1 failure
+ */
+int Rast_legal_bandref(const char *bandref)
+{
+    char **tokens;
+    int ntok;
+
+    if (strlen(bandref) >= GNAME_MAX) {
+        G_warning(_("Band reference is too long"));
+        return -1;
+    }
+
+    if (G_legal_filename(bandref) != 1)
+        return -1;
+
+    tokens = G_tokenize(bandref, "_");
+    ntok = G_number_of_tokens(tokens);
+    if (ntok < 2) {
+        G_warning(_("Band reference must be in form <shortcut>_<bandname>"));
+        G_free_tokens(tokens);
+        return -1;
+    }
+
+    if (strlen(tokens[1]) < 1) {
+        G_free_tokens(tokens);
+        return -1;
+    }
+
+    G_free_tokens(tokens);
+    return 1;
+}
 
 /*!
  * \brief Read the first line of a file in cell_misc/
@@ -138,14 +219,16 @@ static void misc_write_line(const char *elem, const char *name, const char *str)
     FILE *fp;
 
     fp = G_fopen_new_misc("cell_misc", elem, name);
-    if (!fp)
-	G_fatal_error(_("Unable to create <%s> metadata file for raster map <%s@%s>"),
-		      elem, name, G_mapset());
-
-    fprintf(fp, "%s\n", str);
-
-    if (fclose(fp) != 0)
-	G_fatal_error(_("Error closing <%s> metadata file for raster map <%s@%s>"),
-		      elem, name, G_mapset());
+    if (!fp) {
+        G_fatal_error(_("Unable to create <%s> metadata file for raster map <%s@%s>"),
+            elem, name, G_mapset());
+    } /* This else block is unnecessary but helps to silence static code analysis tools */
+    else {
+        fprintf(fp, "%s\n", str);
+
+        if (fclose(fp) != 0)
+            G_fatal_error(_("Error closing <%s> metadata file for raster map <%s@%s>"),
+                elem, name, G_mapset());
+    }
 }
 

+ 108 - 0
lib/raster/testsuite/test_raster_metadata.py

@@ -0,0 +1,108 @@
+"""Test of raster library metadata handling
+
+@author Maris Nartiss
+
+@copyright 2021 by Maris Nartiss and the GRASS Development Team
+
+@license This program is free software under the GNU General Public License (>=v2).
+Read the file COPYING that comes with GRASS
+for details
+"""
+import random
+import string
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+from grass.script.core import tempname
+from grass.pygrass.gis import Mapset
+from grass.pygrass import utils
+
+from grass.lib.gis import G_remove_misc
+from grass.lib.raster import Rast_legal_bandref, Rast_read_bandref, Rast_write_bandref
+
+
+class RastLegalBandIdTestCase(TestCase):
+    def test_empty_name(self):
+        ret = Rast_legal_bandref("")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref(" ")
+        self.assertEqual(ret, -1)
+
+    def test_illegal_name(self):
+        ret = Rast_legal_bandref(".a")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("1")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("1a")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("a/b")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("a@b")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("a#b")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("GRASS")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("USER")
+        self.assertEqual(ret, -1)
+
+    def test_no_second_token(self):
+        ret = Rast_legal_bandref("GRASS_")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("USER_")
+        self.assertEqual(ret, -1)
+        ret = Rast_legal_bandref("S2_")
+        self.assertEqual(ret, -1)
+
+    def test_too_long(self):
+        ret = Rast_legal_bandref(
+            "a_" + "".join(random.choices(string.ascii_letters, k=253))
+        )
+        self.assertEqual(ret, 1)
+        ret = Rast_legal_bandref(
+            "a_" + "".join(random.choices(string.ascii_letters, k=254))
+        )
+        self.assertEqual(ret, -1)
+
+    def test_good_name(self):
+        ret = Rast_legal_bandref("S2_1")
+        self.assertEqual(ret, 1)
+        ret = Rast_legal_bandref("GRASS_aspect_deg")
+        self.assertEqual(ret, 1)
+
+
+class RastBandReferenceTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.map = tempname(10)
+        cls.mapset = Mapset().name
+        cls.bandref = "The_Doors"
+        cls.use_temp_region()
+        cls.runModule("g.region", n=1, s=0, e=1, w=0, res=1)
+        cls.runModule("r.mapcalc", expression="{} = 1".format(cls.map))
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.del_temp_region()
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map)
+
+    def test_read_bandref_present(self):
+        Rast_write_bandref(self.map, self.bandref)
+        ret = utils.decode(Rast_read_bandref(self.map, self.mapset))
+        self.assertEqual(ret, self.bandref)
+
+    def test_read_bandref_absent(self):
+        G_remove_misc("cell_misc", "bandref", self.map)
+        ret = Rast_read_bandref(self.map, self.mapset)
+        self.assertFalse(bool(ret))
+
+    def test_write_bandref(self):
+        G_remove_misc("cell_misc", "bandref", self.map)
+        Rast_write_bandref(self.map, self.bandref)
+        ret = utils.decode(Rast_read_bandref(self.map, self.mapset))
+        self.assertEqual(ret, self.bandref)
+
+
+if __name__ == "__main__":
+    test()

+ 23 - 35
python/grass/pygrass/raster/abstract.py

@@ -162,63 +162,51 @@ class Info(object):
     def mtype(self):
         return RTYPE_STR[libraster.Rast_map_type(self.name, self.mapset)]
 
-    def _get_band_reference(self):
+    def _get_bandref(self):
         """Get band reference identifier.
 
         :return str: band identifier (eg. S2_1) or None
         """
-        band_ref = None
-        p_filename = ctypes.c_char_p()
-        p_band_ref = ctypes.c_char_p()
-        ret = libraster.Rast_read_band_reference(
-            self.name, self.mapset, ctypes.byref(p_filename), ctypes.byref(p_band_ref)
-        )
-        if ret:
-            band_ref = utils.decode(p_band_ref.value)
-            libgis.G_free(p_filename)
-            libgis.G_free(p_band_ref)
 
-        return band_ref
+        bandref = libraster.Rast_read_bandref(self.name, self.mapset)
+        if bandref:
+            return utils.decode(bandref)
+        return None
 
     @must_be_in_current_mapset
-    def _set_band_reference(self, band_reference):
+    def _set_bandref(self, bandref):
         """Set/Unset band reference identifier.
 
-        :param str band_reference: band reference to assign or None to remove (unset)
+        :param str bandref: band reference to assign or None to remove (unset)
         """
-        if band_reference:
-            # assign
-            from grass.bandref import BandReferenceReader, BandReferenceReaderError
-
-            reader = BandReferenceReader()
-            # determine filename (assuming that band_reference is unique!)
-            try:
-                filename = reader.find_file(band_reference)
-            except BandReferenceReaderError as e:
-                fatal("{}".format(e))
-                raise
-            if not filename:
-                fatal("Band reference <{}> not found".format(band_reference))
-                raise
-
-            # write band reference
-            libraster.Rast_write_band_reference(self.name, filename, band_reference)
+        if bandref:
+            if libraster.Rast_legal_bandref(bandref) < 0:
+                raise ValueError(_("Invalid band reference"))
+            libraster.Rast_write_bandref(self.name, bandref)
         else:
-            libraster.Rast_remove_band_reference(self.name)
+            libgis.G_remove_misc("cell_misc", "bandref", self.name)
 
-    band_reference = property(fget=_get_band_reference, fset=_set_band_reference)
+    bandref = property(_get_bandref, _set_bandref)
 
     def _get_units(self):
-        return libraster.Rast_read_units(self.name, self.mapset)
+        units = libraster.Rast_read_units(self.name, self.mapset)
+        if units:
+            return utils.decode(units)
+        return None
 
+    @must_be_in_current_mapset
     def _set_units(self, units):
         libraster.Rast_write_units(self.name, units)
 
     units = property(_get_units, _set_units)
 
     def _get_vdatum(self):
-        return libraster.Rast_read_vdatum(self.name, self.mapset)
+        vdatum = libraster.Rast_read_vdatum(self.name, self.mapset)
+        if vdatum:
+            return utils.decode(vdatum)
+        return None
 
+    @must_be_in_current_mapset
     def _set_vdatum(self, vdatum):
         libraster.Rast_write_vdatum(self.name, vdatum)
 

+ 32 - 59
python/grass/temporal/c_libraries_interface.py

@@ -14,7 +14,7 @@ from grass.exceptions import FatalError
 import sys
 from multiprocessing import Process, Lock, Pipe
 import logging
-from ctypes import byref, cast, c_char_p, c_int, c_void_p, CFUNCTYPE, POINTER
+from ctypes import byref, cast, c_int, c_void_p, CFUNCTYPE, POINTER
 from datetime import datetime
 import grass.lib.gis as libgis
 import grass.lib.raster as libraster
@@ -463,7 +463,7 @@ def _write_timestamp(lock, conn, data):
     try:
         maptype = data[1]
         name = data[2]
-        mapset = data[3]
+        # mapset = data[3]
         layer = data[4]
         timestring = data[5]
         ts = libgis.TimeStamp()
@@ -529,11 +529,8 @@ def _read_band_reference(lock, conn, data):
     """Read the file based GRASS band identifier
     the result using the provided pipe.
 
-    The tuple to be send via pipe: (return value of
-    Rast_read_band_reference).
-
-    Please have a look at the documentation of
-    Rast_read_band_reference, for the return values description.
+    The result to be sent via pipe is the return value of
+    Rast_read_bandref: either a band reference string or None.
 
     :param lock: A multiprocessing.Lock instance
     :param conn: A multiprocessing.Pipe instance used to send True or False
@@ -541,24 +538,19 @@ def _read_band_reference(lock, conn, data):
                  mapset, layer, timestring]
 
     """
-    check = False
-    band_ref = None
+    bandref = None
     try:
         maptype = data[1]
         name = data[2]
         mapset = data[3]
-        layer = data[4]
+        # layer = data[4]
 
         if maptype == RPCDefs.TYPE_RASTER:
-            p_filename = c_char_p()
-            p_band_ref = c_char_p()
-            check = libraster.Rast_read_band_reference(
-                name, mapset, byref(p_filename), byref(p_band_ref)
-            )
-            if check:
-                band_ref = decode(p_band_ref.value)
-                libgis.G_free(p_filename)
-                libgis.G_free(p_band_ref)
+            # Must use temporary variable to work around
+            # ValueError: ctypes objects containing pointers cannot be pickled
+            ret = libraster.Rast_read_bandref(name, mapset)
+            if ret:
+                bandref = decode(ret)
         else:
             logging.error(
                 "Unable to read band reference. " "Unsupported map type %s" % maptype
@@ -567,20 +559,17 @@ def _read_band_reference(lock, conn, data):
     except:
         raise
     finally:
-        conn.send((check, band_ref))
+        conn.send(bandref)
 
 
 ###############################################################################
 
 
 def _write_band_reference(lock, conn, data):
-    """Write the file based GRASS band identifier
-    the return values of the called C-functions using the provided pipe.
+    """Write the file based GRASS band identifier.
 
-    The value to be send via pipe is the return value of Rast_write_band_reference.
-
-    Please have a look at the documentation of
-    Rast_write_band_reference, for the return values description.
+    Rises ValueError on invalid band reference.
+    Always sends back True.
 
     :param lock: A multiprocessing.Lock instance
     :param conn: A multiprocessing.Pipe instance used to send True or False
@@ -588,22 +577,17 @@ def _write_band_reference(lock, conn, data):
                  mapset, layer, timestring]
 
     """
-    check = -3
     try:
         maptype = data[1]
         name = data[2]
-        mapset = data[3]
-        layer = data[4]
-        band_reference = data[5]
+        # mapset = data[3]
+        # layer = data[4]
+        bandref = data[5]
 
         if maptype == RPCDefs.TYPE_RASTER:
-            from grass.bandref import BandReferenceReader
-
-            reader = BandReferenceReader()
-            # determine filename (assuming that band_reference is unique!)
-            filename = reader.find_file(band_reference)
-
-            check = libraster.Rast_write_band_reference(name, filename, band_reference)
+            if libraster.Rast_legal_bandref(bandref) < 0:
+                raise ValueError(_("Invalid band reference"))
+            libraster.Rast_write_bandref(name, bandref)
         else:
             logging.error(
                 "Unable to write band reference. " "Unsupported map type %s" % maptype
@@ -612,20 +596,16 @@ def _write_band_reference(lock, conn, data):
     except:
         raise
     finally:
-        conn.send(check)
+        conn.send(True)
 
 
 ###############################################################################
 
 
 def _remove_band_reference(lock, conn, data):
-    """Remove the file based GRASS band identifier
-    the return values of the called C-functions using the provided pipe.
+    """Remove the file based GRASS band identifier.
 
-    The value to be send via pipe is the return value of Rast_remove_band_reference.
-
-    Please have a look at the documentation of
-    Rast_remove_band_reference, for the return values description.
+    The value to be send via pipe is the return value of G_remove_misc.
 
     :param lock: A multiprocessing.Lock instance
     :param conn: A multiprocessing.Pipe instance used to send True or False
@@ -637,11 +617,11 @@ def _remove_band_reference(lock, conn, data):
     try:
         maptype = data[1]
         name = data[2]
-        mapset = data[3]
-        layer = data[4]
+        # mapset = data[3]
+        # layer = data[4]
 
         if maptype == RPCDefs.TYPE_RASTER:
-            check = libraster.Rast_remove_band_reference(name)
+            check = libgis.G_remove_misc("cell_misc", "bandref", name)
         else:
             logging.error(
                 "Unable to remove band reference. " "Unsupported map type %s" % maptype
@@ -1472,12 +1452,9 @@ class CLibrariesInterface(RPCServerBase):
     def remove_raster_band_reference(self, name, mapset):
         """Remove a file based raster band reference
 
-        Please have a look at the documentation Rast_remove_band_reference
-        for the return values description.
-
         :param name: The name of the map
         :param mapset: The mapset of the map
-        :returns: The return value of Rast_remove_band_reference
+        :returns: The return value of G_remove_misc
         """
         self.check_server()
         self.client_conn.send(
@@ -1488,12 +1465,11 @@ class CLibrariesInterface(RPCServerBase):
     def read_raster_band_reference(self, name, mapset):
         """Read a file based raster band reference
 
-        Please have a look at the documentation Rast_read_band_reference
-        for the return values description.
+        Returns band reference or None
 
         :param name: The name of the map
         :param mapset: The mapset of the map
-        :returns: The return value of Rast_read_band_reference
+        :returns: The return value of Rast_read_bandref
         """
         self.check_server()
         self.client_conn.send(
@@ -1504,16 +1480,13 @@ class CLibrariesInterface(RPCServerBase):
     def write_raster_band_reference(self, name, mapset, band_reference):
         """Write a file based raster band reference
 
-        Please have a look at the documentation Rast_write_band_reference
-        for the return values description.
-
         Note:
-            Only band references of maps from the current mapset can written.
+            Only band references of maps from the current mapset can be written.
 
         :param name: The name of the map
         :param mapset: The mapset of the map
         :param band_reference: band reference identifier
-        :returns: The return value of Rast_write_band_reference
+        :returns: always True
         """
         self.check_server()
         self.client_conn.send(

+ 4 - 4
python/grass/temporal/space_time_datasets.py

@@ -356,11 +356,11 @@ class RasterDataset(AbstractMapDataset):
                  read (due to an error or because not being present)
         """
 
-        check, band_ref = self.ciface.read_raster_band_reference(
+        band_ref = self.ciface.read_raster_band_reference(
             self.get_name(), self.get_mapset()
         )
 
-        if check < 1:
+        if not band_ref:
             return False
 
         self.metadata.set_band_reference(band_ref)
@@ -441,10 +441,10 @@ class RasterDataset(AbstractMapDataset):
             self.metadata.set_number_of_cells(ncells)
 
             # Fill band reference if defined
-            check, band_ref = self.ciface.read_raster_band_reference(
+            band_ref = self.ciface.read_raster_band_reference(
                 self.get_name(), self.get_mapset()
             )
-            if check > 0:
+            if band_ref:
                 self.metadata.set_band_reference(band_ref)
 
             return True

+ 7 - 3
raster/r.info/main.c

@@ -45,7 +45,7 @@ int main(int argc, char **argv)
     const char *title;
     char tmp1[100], tmp2[100], tmp3[100];
     char timebuff[256];
-    char *units, *vdatum;
+    char *units, *vdatum, *bandref;
     int i;
     CELL mincat = 0, maxcat = 0, cat;
     FILE *out;
@@ -120,6 +120,8 @@ int main(int argc, char **argv)
 
     vdatum = Rast_read_vdatum(name, "");
 
+    bandref = Rast_read_bandref(name, "");
+
     /*Check the Timestamp */
     time_ok = G_read_raster_timestamp(name, "", &ts) > 0;
     /*Check for valid entries, show none if no timestamp available */
@@ -178,10 +180,11 @@ int main(int argc, char **argv)
 		     "  Type of Map:  %-20.20s Number of Categories: %-9s",
 		     hist_ok ? Rast_get_history(&hist, HIST_MAPTYPE) : "??", cats_ok ? tmp1 : "??");
 
-	compose_line(out, "  Data Type:    %s",
+	compose_line(out, "  Data Type:    %-20.20s Band reference: %s ",
 		     (data_type == CELL_TYPE ? "CELL" :
 		      (data_type == DCELL_TYPE ? "DCELL" :
-		       (data_type == FCELL_TYPE ? "FCELL" : "??"))));
+		       (data_type == FCELL_TYPE ? "FCELL" : "??"))),
+             (bandref ? bandref : "(none)"));
 
 	/* For now hide these unless they exist to keep the noise low. In
 	 *   future when the two are used more widely they can be printed
@@ -566,6 +569,7 @@ int main(int argc, char **argv)
 	    }
 	    fprintf(out, "units=%s\n", units ? units : "\"none\"");
 	    fprintf(out, "vdatum=%s\n", vdatum ? vdatum : "\"none\"");
+        fprintf(out, "bandref=%s\n", bandref ? bandref : "\"none\"");
 	    fprintf(out, "source1=\"%s\"\n", hist_ok ? Rast_get_history(&hist, HIST_DATSRC_1) : "\"none\"");
 	    fprintf(out, "source2=\"%s\"\n", hist_ok ? Rast_get_history(&hist, HIST_DATSRC_2) : "\"none\"");
 	    fprintf(out, "description=\"%s\"\n", hist_ok ? Rast_get_history(&hist, HIST_KEYWRD) : "\"none\"");

+ 1 - 0
raster/r.info/r.info.html

@@ -116,6 +116,7 @@ title="South-West Wake county: slope in degrees (slope_ned10m)"
 timestamp="none"
 units="none"
 vdatum="none"
+bandref="none"
 source1="raster elevation file elev_ned10m"
 source2=""
 description="generated by r.slope.aspect"

+ 45 - 2
raster/r.support/main.c

@@ -40,9 +40,10 @@ int main(int argc, char *argv[])
     struct GModule *module;
     struct Option *raster, *title_opt, *history_opt;
     struct Option *datasrc1_opt, *datasrc2_opt, *datadesc_opt;
+    struct Option *bandref_opt;
     struct Option *map_opt, *units_opt, *vdatum_opt;
     struct Option *load_opt, *save_opt;
-    struct Flag *stats_flag, *null_flag, *del_flag;
+    struct Flag *stats_flag, *null_flag, *del_flag, *bandref_rm_flag;
     int is_reclass;		/* Is raster reclass? */
     const char *infile;
     struct History hist;
@@ -63,6 +64,7 @@ int main(int argc, char *argv[])
     title_opt->key_desc = "phrase";
     title_opt->type = TYPE_STRING;
     title_opt->required = NO;
+    title_opt->guisection = _("Metadata");
     title_opt->description = _("Title for resultant raster map");
 
     history_opt = G_define_option();
@@ -70,6 +72,7 @@ int main(int argc, char *argv[])
     history_opt->key_desc = "phrase";
     history_opt->type = TYPE_STRING;
     history_opt->required = NO;
+    history_opt->guisection = _("Metadata");
     history_opt->description =
 	_("Text to append to the next line of the map's metadata file");
 
@@ -77,12 +80,14 @@ int main(int argc, char *argv[])
     units_opt->key = "units";
     units_opt->type = TYPE_STRING;
     units_opt->required = NO;
+    units_opt->guisection = _("Metadata");
     units_opt->description = _("Text to use for map data units");
 
     vdatum_opt = G_define_option();
     vdatum_opt->key = "vdatum";
     vdatum_opt->type = TYPE_STRING;
     vdatum_opt->required = NO;
+    vdatum_opt->guisection = _("Metadata");
     vdatum_opt->description = _("Text to use for map vertical datum");
 
     datasrc1_opt = G_define_option();
@@ -90,6 +95,7 @@ int main(int argc, char *argv[])
     datasrc1_opt->key_desc = "phrase";
     datasrc1_opt->type = TYPE_STRING;
     datasrc1_opt->required = NO;
+    datasrc1_opt->guisection = _("Metadata");
     datasrc1_opt->description = _("Text to use for data source, line 1");
 
     datasrc2_opt = G_define_option();
@@ -97,6 +103,7 @@ int main(int argc, char *argv[])
     datasrc2_opt->key_desc = "phrase";
     datasrc2_opt->type = TYPE_STRING;
     datasrc2_opt->required = NO;
+    datasrc2_opt->guisection = _("Metadata");
     datasrc2_opt->description = _("Text to use for data source, line 2");
 
     datadesc_opt = G_define_option();
@@ -104,6 +111,7 @@ int main(int argc, char *argv[])
     datadesc_opt->key_desc = "phrase";
     datadesc_opt->type = TYPE_STRING;
     datadesc_opt->required = NO;
+    datadesc_opt->guisection = _("Metadata");
     datadesc_opt->description =
 	_("Text to use for data description or keyword(s)");
 
@@ -112,28 +120,48 @@ int main(int argc, char *argv[])
     map_opt->type = TYPE_STRING;
     map_opt->required = NO;
     map_opt->gisprompt = "old,cell,raster";
+    map_opt->guisection = _("Import / Export");
     map_opt->description = _("Raster map from which to copy category table");
 
     load_opt = G_define_standard_option(G_OPT_F_INPUT);
     load_opt->key = "loadhistory";
     load_opt->required = NO;
+    load_opt->guisection = _("Import / Export");
     load_opt->description = _("Text file from which to load history");
 
     save_opt = G_define_standard_option(G_OPT_F_OUTPUT);
     save_opt->key = "savehistory";
     save_opt->required = NO;
+    save_opt->guisection = _("Import / Export");
     save_opt->description = _("Text file in which to save history");
 
+    bandref_opt = G_define_option();
+    bandref_opt->key = "bandref";
+    bandref_opt->key_desc = "phrase";
+    bandref_opt->type = TYPE_STRING;
+    bandref_opt->required = NO;
+    bandref_opt->guisection = _("Band reference");
+    bandref_opt->description =
+	_("Band reference in form shortcut_name e.g. S2_8A");
+
+    bandref_rm_flag = G_define_flag();
+    bandref_rm_flag->key = 'b';
+    bandref_rm_flag->guisection = _("Band reference");
+    bandref_rm_flag->description = _("Delete the band reference");
+
     stats_flag = G_define_flag();
     stats_flag->key = 's';
+    stats_flag->guisection = _("Maintenance");
     stats_flag->description = _("Update statistics (histogram, range)");
 
     null_flag = G_define_flag();
     null_flag->key = 'n';
+    null_flag->guisection = _("Maintenance");
     null_flag->description = _("Create/reset the null file");
 
     del_flag = G_define_flag();
     del_flag->key = 'd';
+    del_flag->guisection = _("Maintenance");
     del_flag->description = _("Delete the null file");
 
     /* Parse command-line options */
@@ -146,6 +174,10 @@ int main(int argc, char *argv[])
     if (!mapset || strcmp(mapset, G_mapset()) != 0)
 	G_fatal_error(_("Raster map <%s> not found in current mapset"), infile);
 
+    if (bandref_rm_flag->answer && bandref_opt->answer)
+        G_fatal_error(_("Band reference removal and setting band "
+                        "reference values simultaneously doesn't make sense"));
+
     Rast_get_cellhd(raster->answer, "", &cellhd);
     is_reclass = (Rast_is_reclass(raster->answer, "", rname, rmapset) > 0);
 
@@ -252,10 +284,21 @@ int main(int argc, char *argv[])
 	Rast_free_cats(&cats);
     }
 
+    if (bandref_opt->answer) {
+        if (Rast_legal_bandref(bandref_opt->answer) < 0)
+            G_fatal_error(_("Provided band reference is not valid. "
+                            "See documentation for valid examples"));
+
+        Rast_write_bandref(infile, bandref_opt->answer);
+    }
+
+    if (bandref_rm_flag->answer)
+        G_remove_misc("cell_misc", "bandref", infile);
 
     if (title_opt->answer || history_opt->answer || units_opt->answer
 	|| vdatum_opt->answer || datasrc1_opt->answer || datasrc2_opt->answer
-	|| datadesc_opt->answer || map_opt->answer)
+	|| datadesc_opt->answer || map_opt->answer
+	|| bandref_opt->answer || bandref_rm_flag->answer)
 	exit(EXIT_SUCCESS);
 
 

+ 18 - 8
raster/r.support/r.support.html

@@ -2,8 +2,17 @@
 
 <b>r.support</b> allows the user to create and/or edit raster map support 
 information. Editing of raster map color tables, category labels, header, 
-history, and title is supported. Category labels can also be copied from
-another raster map.
+history, band reference elements and title is supported.
+Category labels can also be copied from another raster map.
+
+<h3>Raster band management</h3>
+Raster band reference concept is similar to dimension name in other GIS and
+remote sensing applications. Most common usage will be assigning a
+remote sensing platform sensor band ID to the raster, although any
+identifier is supported. Raster band reference is required to work with
+imagery classification tools.<br>
+Band reference should be in form &lt;shortcut&gt;_&lt;bandname&gt; e.g.
+use <b>L5_1</b> if raster contains data from Landsat 5 visible blue (1) band.
 
 <h2>EXAMPLES</h2>
 These examples are based on the North Carolina dataset, more specfically the <em>landuse</em> raster map.
@@ -28,6 +37,11 @@ Copy the landuse map to the current mapset
 <div class="code"><pre>r.support map=my_landuse units=meter
 </pre></div>
 
+<h3>Set band reference</h3>
+Note: landuse map doesn't confirm to CORINE specification. This is an example only.
+<div class="code"><pre>r.support map=my_landuse bandref=CORINE_LULC
+</pre></div>
+
 <h2>NOTES</h2>
 
 If metadata options such as <b>title</b> or <b>history</b> are given the
@@ -57,10 +71,6 @@ All other metadata strings available as standard options are limited to
 Micharl Shapiro, CERL: Original author<br>
 <a href="MAILTO:rez@touchofmadness.com">Brad Douglas</a>: GRASS 6 Port<br>
 M. Hamish Bowman: command line enhancements<br>
-Markus Neteler: category copy from other map
-
-<!--
-<p>
-<i>Last changed: $Date$</i>
--->
+Markus Neteler: category copy from other map<br>
+Maris Nartiss: band reference management
 

+ 66 - 0
raster/r.support/testsuite/test_r_support.py

@@ -0,0 +1,66 @@
+"""Test of r.support basic functionality
+
+@author Maris Nartiss
+
+@copyright 2021 by Maris Nartiss and the GRASS Development Team
+
+@license This program is free software under the GNU General Public License (>=v2).
+Read the file COPYING that comes with GRASS
+for details
+"""
+import random
+import string
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+from grass.script.core import tempname
+from grass.pygrass.gis import Mapset
+from grass.pygrass import utils
+
+from grass.lib.gis import G_remove_misc
+from grass.lib.raster import Rast_read_bandref
+
+
+class RSupportBandHandlingTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.map = tempname(10)
+        cls.mapset = Mapset().name
+        cls.bandref = "The_Doors"
+        cls.use_temp_region()
+        cls.runModule("g.region", n=1, s=0, e=1, w=0, res=1)
+        cls.runModule("r.mapcalc", expression="{} = 1".format(cls.map))
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.del_temp_region()
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map)
+
+    def test_exclusitivity(self):
+        self.assertModuleFail("r.support", map=self.map, bandref=self.bandref, b=True)
+
+    def test_bandref_invalid(self):
+        self.assertModuleFail(
+            "r.support",
+            map=self.map,
+            bandref="".join(random.choices(string.ascii_letters, k=256)),
+        )
+
+    def test_set_bandref(self):
+        G_remove_misc("cell_misc", "bandref", self.map)
+        self.assertModule("r.support", map=self.map, bandref=self.bandref)
+        ret = utils.decode(Rast_read_bandref(self.map, self.mapset))
+        self.assertEqual(ret, self.bandref)
+
+    def test_remove_band_ref(self):
+        self.assertModule("r.support", map=self.map, bandref=self.bandref)
+        ret = Rast_read_bandref(self.map, self.mapset)
+        self.assertTrue(bool(ret))
+        self.assertModule("r.support", map=self.map, b=True)
+        ret = Rast_read_bandref(self.map, self.mapset)
+        self.assertFalse(bool(ret))
+
+
+if __name__ == "__main__":
+    test()

+ 2 - 1
scripts/i.band/i.band.html

@@ -78,7 +78,8 @@ raster maps.
 
 <em>
   <a href="g.bands.html">g.bands</a>,
-  <a href="r.info.html">r.info</a>
+  <a href="r.info.html">r.info</a>,
+  <a href="r.support">r.support</a>
 </em>
 
 <h2>AUTHORS</h2>

+ 2 - 2
scripts/i.band/i.band.py

@@ -57,7 +57,7 @@ def print_map_band_reference(name, band_reader):
 
     try:
         with RasterRow(name) as rast:
-            band_ref = rast.info.band_reference
+            band_ref = rast.info.bandref
             if band_ref:
                 shortcut, band = band_ref.split("_")
                 band_reader.print_info(shortcut, band)
@@ -91,7 +91,7 @@ def manage_map_band_reference(name, band_ref):
                     _("Band reference dissociated from raster map <{}>").format(name), 1
                 )
             try:
-                rast.info.band_reference = band_ref
+                rast.info.bandref = band_ref
             except GrassError as e:
                 gs.error(_("Unable to assign/dissociate band reference. {}").format(e))
                 return 1

+ 25 - 16
scripts/i.band/testsuite/test_i_band.py

@@ -2,39 +2,48 @@ from grass.gunittest.case import TestCase
 from grass.gunittest.main import test
 from grass.gunittest.gmodules import SimpleModule, call_module
 
+from grass.script.core import tempname
+from grass.pygrass.gis import Mapset
 from grass.pygrass.raster import RasterRow
 
 
 class TestBandsSystemDefined(TestCase):
-    # note that full NC dataset is needed
-    raster_map = "lsat7_2002_10"
-    band_ref = "L7_1"
+    @classmethod
+    def setUpClass(cls):
+        cls.map = tempname(10)
+        cls.bandref = "The_Doors"
+        cls.mapset = Mapset()
+        cls.use_temp_region()
+        cls.runModule("g.region", n=1, s=0, e=1, w=0, res=1)
+        cls.runModule("r.mapcalc", expression="{} = 1".format(cls.map))
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.del_temp_region()
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map)
 
     def read_band_ref(self):
-        with RasterRow(self.raster_map) as rast:
-            band_ref = rast.info.band_reference
+        with RasterRow(self.map) as rast:
+            band_ref = rast.info.bandref
 
         return band_ref
 
     def test_band_ref_assign_not_current_mapset(self):
-        # it is assumed that we are not in PERMANENT mapset
-        module = SimpleModule(
-            "i.band", map=self.raster_map + "@PERMANENT", band=self.band_ref
-        )
-        self.assertModuleFail(module)
+        if not self.mapset == "PERMANENT":
+            self.mapset.name = "PERMANENT"
+            a_map = self.mapset.glist(type="raster")[0]
+            module = SimpleModule("i.band", map=a_map, band=self.bandref)
+            self.assertModuleFail(module)
 
     def test_band_ref_assign(self):
-        # Copy raster map to the current mapset
-        call_module("g.copy", raster="{m}@PERMANENT,{m}".format(m=self.raster_map))
-
-        module = SimpleModule("i.band", map=self.raster_map, band=self.band_ref)
+        module = SimpleModule("i.band", map=self.map, band=self.bandref)
         self.assertModule(module)
 
         # check also using pygrass
-        self.assertEqual(self.read_band_ref(), self.band_ref)
+        self.assertEqual(self.read_band_ref(), self.bandref)
 
     def test_band_ref_dissociate(self):
-        module = SimpleModule("i.band", operation="remove", map=self.raster_map)
+        module = SimpleModule("i.band", operation="remove", map=self.map)
         self.assertModule(module)
 
         # check also using pygrass