Procházet zdrojové kódy

Band references implementation (#63)

Implements image collection support into GRASS GIS
Martin Landa před 5 roky
rodič
revize
a83af32aac
37 změnil soubory, kde provedl 2038 přidání a 38 odebrání
  1. 4 0
      include/defs/gis.h
  2. 6 0
      include/defs/raster.h
  3. 78 0
      lib/gis/band_reference.c
  4. 1 1
      lib/python/Makefile
  5. 27 0
      lib/python/bandref/Makefile
  6. 1 0
      lib/python/bandref/__init__.py
  7. 195 0
      lib/python/bandref/reader.py
  8. 13 0
      lib/python/pygrass/errors.py
  9. 54 1
      lib/python/pygrass/raster/abstract.py
  10. 89 0
      lib/python/temporal/abstract_space_time_dataset.py
  11. 183 1
      lib/python/temporal/c_libraries_interface.py
  12. 1 1
      lib/python/temporal/core.py
  13. 30 14
      lib/python/temporal/mapcalc.py
  14. 38 1
      lib/python/temporal/metadata.py
  15. 16 5
      lib/python/temporal/open_stds.py
  16. 36 1
      lib/python/temporal/register.py
  17. 65 0
      lib/python/temporal/space_time_datasets.py
  18. 126 0
      lib/raster/band_reference.c
  19. 1 0
      lib/temporal/SQL/raster_metadata_table.sql
  20. 2 2
      lib/temporal/SQL/raster_views.sql
  21. 1 0
      lib/temporal/SQL/strds_metadata_table.sql
  22. 6 0
      lib/temporal/SQL/update_strds_metadata_template.sql
  23. 2 0
      scripts/Makefile
  24. 18 0
      scripts/g.bands/Makefile
  25. 214 0
      scripts/g.bands/g.bands.html
  26. 79 0
      scripts/g.bands/g.bands.py
  27. 159 0
      scripts/g.bands/landsat.json
  28. 167 0
      scripts/g.bands/sentinel.json
  29. 42 0
      scripts/g.bands/testsuite/test_g_bands.py
  30. 7 0
      scripts/i.band/Makefile
  31. 89 0
      scripts/i.band/i.band.html
  32. 129 0
      scripts/i.band/i.band.py
  33. 44 0
      scripts/i.band/testsuite/test_i_band.py
  34. 25 0
      temporal/t.info/t.info.html
  35. 26 0
      temporal/t.rast.list/t.rast.list.html
  36. 25 11
      temporal/t.rast.mapcalc/t.rast.mapcalc.html
  37. 39 0
      temporal/t.register/t.register.html

+ 4 - 0
include/defs/gis.h

@@ -161,6 +161,10 @@ int G_asprintf(char **, const char *, ...)
 int G_rasprintf(char **, size_t *,const char *, ...)
     __attribute__ ((format(printf, 3, 4)));
 
+/* 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 *);

+ 6 - 0
include/defs/raster.h

@@ -35,6 +35,12 @@ 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 *);

+ 78 - 0
lib/gis/band_reference.c

@@ -0,0 +1,78 @@
+/*!
+ * \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;
+}
+

+ 1 - 1
lib/python/Makefile

@@ -5,7 +5,7 @@ include $(MODULE_TOPDIR)/include/Make/Python.make
 
 PYDIR = $(ETC)/python/grass
 
-SUBDIRS = exceptions script ctypes temporal pygrass pydispatch imaging gunittest
+SUBDIRS = exceptions script ctypes temporal pygrass pydispatch imaging gunittest bandref
 
 default: $(PYDIR)/__init__.py
 	$(MAKE) subdirs

+ 27 - 0
lib/python/bandref/Makefile

@@ -0,0 +1,27 @@
+MODULE_TOPDIR = ../../..
+
+include $(MODULE_TOPDIR)/include/Make/Other.make
+include $(MODULE_TOPDIR)/include/Make/Python.make
+
+PYDIR = $(ETC)/python
+GDIR = $(PYDIR)/grass
+DSTDIR = $(GDIR)/bandref
+
+MODULES = reader
+
+PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__)
+PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__)
+
+default: $(PYFILES) $(PYCFILES) $(GDIR)/__init__.py $(GDIR)/__init__.pyc
+
+$(PYDIR):
+	$(MKDIR) $@
+
+$(GDIR): | $(PYDIR)
+	$(MKDIR) $@
+
+$(DSTDIR): | $(GDIR)
+	$(MKDIR) $@
+
+$(DSTDIR)/%: % | $(DSTDIR)
+	$(INSTALL_DATA) $< $@

+ 1 - 0
lib/python/bandref/__init__.py

@@ -0,0 +1 @@
+from .reader import BandReferenceReader, BandReferenceReaderError

+ 195 - 0
lib/python/bandref/reader.py

@@ -0,0 +1,195 @@
+import os
+import sys
+import json
+import glob
+import re
+from collections import OrderedDict
+
+import grass.script as gs
+
+class BandReferenceReaderError(Exception):
+    pass
+
+class BandReferenceReader:
+    """Band references reader"""
+    def __init__(self):
+        self._json_files = glob.glob(
+            os.path.join(os.environ['GISBASE'], 'etc', 'g.bands', '*.json')
+        )
+        if not self._json_files:
+            raise BandReferenceReaderError("No band definitions found")
+
+        self._read_config()
+
+    def _read_config(self):
+        """Read configuration"""
+        self.config = dict()
+        for json_file in self._json_files:
+            try:
+                with open(json_file) as fd:
+                    config = json.load(
+                        fd,
+                        object_pairs_hook=OrderedDict
+                    )
+            except json.decoder.JSONDecodeError as e:
+                raise BandReferenceReaderError(
+                    "Unable to parse '{}': {}".format(
+                        json_file, e
+                ))
+
+            # check if configuration is valid
+            self._check_config(config)
+
+            self.config[os.path.basename(json_file)] = config
+
+    @staticmethod
+    def _check_config(config):
+        """Check if config is valid
+
+        :todo: check shortcut uniqueness
+
+        :param dict config: configuration to be validated
+        """
+        for items in config.values():
+            for item in ('shortcut', 'bands'):
+                if item not in items.keys():
+                    raise BandReferenceReaderError(
+                        "Invalid band definition: <{}> is missing".format(item
+                ))
+            if len(items['bands']) < 1:
+                raise BandReferenceReaderError(
+                    "Invalid band definition: no bands defined"
+                )
+
+    @staticmethod
+    def _print_band_extended(band, item):
+        """Print band-specific metadata
+
+        :param str band: band identifier
+        :param str item: items to be printed out
+        """
+        def print_kv(k, v, indent):
+            if isinstance(v, OrderedDict):
+                print ('{}{}:'.format(' ' * indent * 2, k))
+                for ki, vi in v.items():
+                    print_kv(ki, vi, indent * 2)
+            else:
+                print ('{}{}: {}'.format(' ' * indent * 2, k, v))
+
+        indent = 4
+        print ('{}band: {}'.format(
+            ' ' * indent, band
+        ))
+        for k, v in item[band].items():
+            print_kv(k, v, indent)
+
+    def _print_band(self, shortcut, band, tag=None):
+        sys.stdout.write(self._band_identifier(shortcut, band))
+        if tag:
+            sys.stdout.write(' {}'.format(tag))
+        sys.stdout.write(os.linesep)
+
+    def print_info(self, shortcut=None, band=None, extended=False):
+        """Prints band reference information to stdout.
+
+        Can be filtered by shortcut or band identifier.
+
+        :param str shortcut: shortcut to filter (eg. S2) or None
+        :param str band: band (eg. 1) or None
+        :param bool extended: print also extended metadata
+        """
+        found = False
+        for root in self.config.values():
+            for item in root.values():
+                try:
+                    if shortcut and re.match(shortcut, item['shortcut']) is None:
+                        continue
+                except re.error as e:
+                    raise BandReferenceReaderError(
+                        "Invalid pattern: {}".format(e)
+                    )
+
+                found = True
+                if band and band not in item['bands']:
+                    raise BandReferenceReaderError(
+                        "Band <{}> not found in <{}>".format(
+                            band, shortcut
+                        ))
+
+                # print generic information
+                if extended:
+                    for subitem in item.keys():
+                        if subitem == 'bands':
+                            # bands item is processed bellow
+                            continue
+                        print ('{}: {}'.format(
+                            subitem, item[subitem]
+                        ))
+
+                    # print detailed band information
+                    if band:
+                        self._print_band_extended(band, item['bands'])
+                    else:
+                        for iband in item['bands']:
+                            self._print_band_extended(iband, item['bands'])
+                else:
+                    # basic information only
+                    if band:
+                        self._print_band(
+                            item['shortcut'], band,
+                            item['bands'][band].get('tag')
+                        )
+                    else:
+                        for iband in item['bands']:
+                            self._print_band(
+                                item['shortcut'], iband,
+                                item['bands'][iband].get('tag')
+                            )
+
+        # raise error when defined shortcut not found
+        if shortcut and not found:
+            raise BandReferenceReaderError(
+                "Band reference <{}> not found".format(shortcut)
+            )
+
+    def find_file(self, band_reference):
+        """Find file by band reference.
+
+        Match is case-insensitive.
+
+        :param str band_reference: band reference identifier to search for (eg. S2_1)
+
+        :return str: file basename if found or None
+        """
+        try:
+            shortcut, band = band_reference.split('_')
+        except ValueError:
+            raise BandReferenceReaderError("Invalid band identifier <{}>".format(
+                band_reference
+            ))
+
+        for filename, config in self.config.items():
+            for root in config.keys():
+                if config[root]['shortcut'].upper() == shortcut.upper() and \
+                   band.upper() in map(lambda x: x.upper(), config[root]['bands'].keys()):
+                    return filename
+
+        return None
+
+    def get_bands(self):
+        """Get list of band identifiers.
+
+        :return list: list of valid band identifiers
+        """
+        bands = []
+        for root in self.config.values():
+            for item in root.values():
+                for band in item['bands']:
+                    bands.append(
+                        self._band_identifier(item['shortcut'], band)
+                    )
+        return bands
+
+    @staticmethod
+    def _band_identifier(shortcut, band):
+        return '{}_{}'.format(shortcut, band)

+ 13 - 0
lib/python/pygrass/errors.py

@@ -5,6 +5,7 @@ from grass.exceptions import (FlagError, ParameterError, DBError,
                               GrassError, OpenError)
 
 from grass.pygrass.messages import get_msgr
+import grass.lib.gis as libgis
 
 
 def must_be_open(method):
@@ -29,3 +30,15 @@ def mapinfo_must_be_set(method):
             raise GrassError(_("The self.c_mapinfo pointer must be "\
                                  "correctly initiated"))
     return wrapper
+
+def must_be_in_current_mapset(method):
+
+    @wraps(method)
+    def wrapper(self, *args, **kargs):
+        if self.mapset == libgis.G_mapset().decode():
+            return method(self, *args, **kargs)
+        else:
+            raise GrassError(_("Map <{}> not found in current mapset").format(
+                self.name))
+
+    return wrapper

+ 54 - 1
lib/python/pygrass/raster/abstract.py

@@ -20,7 +20,7 @@ import grass.lib.raster as libraster
 #
 from grass.pygrass import utils
 from grass.pygrass.gis.region import Region
-from grass.pygrass.errors import must_be_open
+from grass.pygrass.errors import must_be_open, must_be_in_current_mapset
 from grass.pygrass.shell.conversion import dict2html
 from grass.pygrass.shell.show import raw_figure
 
@@ -156,6 +156,53 @@ class Info(object):
     def mtype(self):
         return RTYPE_STR[libraster.Rast_map_type(self.name, self.mapset)]
 
+    def _get_band_reference(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
+
+    @must_be_in_current_mapset
+    def _set_band_reference(self, band_reference):
+        """Set/Unset band reference identifier.
+
+        :param str band_reference: 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)
+        else:
+            libraster.Rast_remove_band_reference(self.name)
+
+    band_reference = property(fget=_get_band_reference, fset=_set_band_reference)
+
     def _get_units(self):
         return libraster.Rast_read_units(self.name, self.mapset)
 
@@ -224,6 +271,12 @@ class RasterAbstractBase(object):
         ..
         """
         self.mapset = mapset
+        if not mapset:
+            # note that @must_be_in_current_mapset requires mapset to be set
+            mapset = libgis.G_find_raster(name, mapset)
+            if mapset is not None:
+                self.mapset = utils.decode(mapset)
+
         self._name = name
         # Private attribute `_fd` that return the file descriptor of the map
         self._fd = None

+ 89 - 0
lib/python/temporal/abstract_space_time_dataset.py

@@ -52,6 +52,29 @@ class AbstractSpaceTimeDataset(AbstractDataset):
         self.reset(ident)
         self.map_counter = 0
 
+        # SpaceTimeRasterDataset related only
+        self.band_reference = None
+
+    def get_name(self, band_reference=True):
+        """Get dataset name including band reference filter if enabled.
+
+        :param bool band_reference: True to return dataset name
+        including band reference filter if defined
+        (eg. "landsat.L8_1") otherwise dataset name is returned only
+        (eg. "landsat").
+
+        :return str: dataset name
+
+        """
+        dataset_name = super(AbstractSpaceTimeDataset, self).get_name()
+
+        if band_reference and self.band_reference:
+            return '{}.{}'.format(
+                dataset_name, self.band_reference
+            )
+
+        return dataset_name
+
     def create_map_register_name(self):
         """Create the name of the map register table of this space time
             dataset
@@ -1447,6 +1470,68 @@ class AbstractSpaceTimeDataset(AbstractDataset):
 
         return obj_list
 
+    def _update_where_statement_by_band_reference(self, where):
+        """Update given SQL WHERE statement by band reference.
+
+        Call this method only when self.band_reference is defined.
+
+        :param str where: SQL WHERE statement to be updated
+
+        :return: updated SQL WHERE statement
+        """
+        def leading_zero(value):
+            try:
+                if value.startswith('0'):
+                    return value.lstrip('0')
+                else:
+                    return '{0:02d}'.format(int(value))
+            except ValueError:
+                return value
+
+            return None
+
+        # initialized WHERE statement
+        if where:
+            where += " AND "
+        else:
+            where = ""
+
+        # be case-insensitive
+        if '_' in self.band_reference:
+            # fully-qualified band reference
+            where += "band_reference IN ('{}'".format(
+                self.band_reference.upper()
+            )
+
+            # be zero-padding less sensitive
+            shortcut, identifier = self.band_reference.split('_', -1)
+            identifier_zp = leading_zero(identifier)
+            if identifier_zp:
+                where += ", '{fl}_{zp}'".format(
+                    fl=shortcut.upper(),
+                    zp=identifier_zp.upper()
+                )
+
+            # close WHERE statement
+            where += ')'
+        else:
+            # shortcut or band identifier given
+            shortcut_identifier = leading_zero(self.band_reference)
+            if shortcut_identifier:
+                where += "{br} LIKE '{si}\_%' {esc} OR {br} LIKE '%\_{si}' {esc} OR " \
+                    "{br} LIKE '{orig}\_%' {esc} OR {br} LIKE '%\_{orig}' {esc}".format(
+                        br="band_reference",
+                        si=shortcut_identifier,
+                        orig=self.band_reference.upper(),
+                        esc="ESCAPE '\\'"
+                )
+            else:
+                where += "band_reference = '{}'".format(
+                    self.band_reference
+                )
+
+        return where
+
     def get_registered_maps(self, columns=None, where=None, order=None,
                             dbif=None):
         """Return SQL rows of all registered maps.
@@ -1485,6 +1570,10 @@ class AbstractSpaceTimeDataset(AbstractDataset):
                 sql = "SELECT * FROM %s  WHERE %s.id IN (SELECT id FROM %s)" % \
                       (map_view, map_view, self.get_map_register())
 
+            # filter by band reference identifier
+            if self.band_reference:
+                where = self._update_where_statement_by_band_reference(where)
+
             if where is not None and where != "":
                 sql += " AND (%s)" % (where.split(";")[0])
             if order is not None and order != "":

+ 183 - 1
lib/python/temporal/c_libraries_interface.py

@@ -27,7 +27,7 @@ from grass.pygrass.rpc.base import RPCServerBase
 from grass.pygrass.raster import RasterRow
 from grass.pygrass.vector import VectorTopo
 from grass.script.utils import encode
-from grass.pygrass.utils import decode
+from grass.pygrass.utils import decode, set_path
 
 ###############################################################################
 
@@ -49,6 +49,9 @@ class RPCDefs(object):
     G_LOCATION = 12
     G_GISDBASE = 13
     READ_MAP_FULL_INFO = 14
+    WRITE_BAND_REFERENCE = 15
+    READ_BAND_REFERENCE = 16
+    REMOVE_BAND_REFERENCE = 17
     G_FATAL_ERROR = 49
 
     TYPE_RASTER = 0
@@ -494,6 +497,133 @@ def _remove_timestamp(lock, conn, data):
 ###############################################################################
 
 
+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.
+
+       :param lock: A multiprocessing.Lock instance
+       :param conn: A multiprocessing.Pipe instance used to send True or False
+       :param data: The list of data entries [function_id, maptype, name,
+                    mapset, layer, timestring]
+
+    """
+    check = False
+    band_ref = None
+    try:
+        maptype = data[1]
+        name = data[2]
+        mapset = data[3]
+        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)
+        else:
+            logging.error("Unable to read band reference. "
+                          "Unsupported map type %s" % maptype)
+            return -1
+    except:
+        raise
+    finally:
+        conn.send((check, band_ref))
+
+###############################################################################
+
+
+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.
+
+       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.
+
+       :param lock: A multiprocessing.Lock instance
+       :param conn: A multiprocessing.Pipe instance used to send True or False
+       :param data: The list of data entries [function_id, maptype, name,
+                    mapset, layer, timestring]
+
+    """
+    check = -3
+    try:
+        maptype = data[1]
+        name = data[2]
+        mapset = data[3]
+        layer = data[4]
+        band_reference = 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)
+        else:
+            logging.error("Unable to write band reference. "
+                          "Unsupported map type %s" % maptype)
+            return -2
+    except:
+        raise
+    finally:
+        conn.send(check)
+
+###############################################################################
+
+
+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.
+
+       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.
+
+       :param lock: A multiprocessing.Lock instance
+       :param conn: A multiprocessing.Pipe instance used to send True or False
+       :param data: The list of data entries [function_id, maptype, name,
+                    mapset, layer, timestring]
+
+    """
+    check = False
+    try:
+        maptype = data[1]
+        name = data[2]
+        mapset = data[3]
+        layer = data[4]
+
+        if maptype == RPCDefs.TYPE_RASTER:
+            check = libraster.Rast_remove_band_reference(name)
+        else:
+            logging.error("Unable to remove band reference. "
+                          "Unsupported map type %s" % maptype)
+            return -2
+    except:
+        raise
+    finally:
+        conn.send(check)
+
+###############################################################################
+
+
 def _map_exists(lock, conn, data):
     """Check if a map exists in the spatial database
 
@@ -950,6 +1080,9 @@ def c_library_server(lock, conn):
     functions[RPCDefs.G_LOCATION] = _get_location
     functions[RPCDefs.G_GISDBASE] = _get_gisdbase
     functions[RPCDefs.READ_MAP_FULL_INFO] = _read_map_full_info
+    functions[RPCDefs.WRITE_BAND_REFERENCE] = _write_band_reference
+    functions[RPCDefs.READ_BAND_REFERENCE] = _read_band_reference
+    functions[RPCDefs.REMOVE_BAND_REFERENCE] = _remove_band_reference
     functions[RPCDefs.G_FATAL_ERROR] = _fatal_error
 
     libgis.G_gisinit("c_library_server")
@@ -1263,6 +1396,55 @@ class CLibrariesInterface(RPCServerBase):
                                name, mapset, None, timestring])
         return self.safe_receive("write_raster_timestamp")
 
+    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
+       """
+        self.check_server()
+        self.client_conn.send([RPCDefs.REMOVE_BAND_REFERENCE, RPCDefs.TYPE_RASTER,
+                               name, mapset, None])
+        return self.safe_receive("remove_raster_timestamp")
+
+    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.
+
+           :param name: The name of the map
+           :param mapset: The mapset of the map
+           :returns: The return value of Rast_read_band_reference
+        """
+        self.check_server()
+        self.client_conn.send([RPCDefs.READ_BAND_REFERENCE, RPCDefs.TYPE_RASTER,
+                               name, mapset, None])
+        return self.safe_receive("read_raster_band_reference")
+
+    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.
+
+           :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
+        """
+        self.check_server()
+        self.client_conn.send([RPCDefs.WRITE_BAND_REFERENCE, RPCDefs.TYPE_RASTER,
+                               name, mapset, None, band_reference])
+        return self.safe_receive("write_raster_band_reference")
+
     def raster3d_map_exists(self, name, mapset):
         """Check if a 3D raster map exists in the spatial database
 

+ 1 - 1
lib/python/temporal/core.py

@@ -113,7 +113,7 @@ tgis_version = 2
 # can differ this value must be an integer larger than 0
 # Increase this value in case of backward incompatible changes
 # temporal database SQL layout
-tgis_db_version = 2
+tgis_db_version = 3
 
 # We need to know the parameter style of the database backend
 tgis_dbmi_paramstyle = None

+ 30 - 14
lib/python/temporal/mapcalc.py

@@ -89,6 +89,16 @@ def dataset_mapcalculator(inputs, output, type, expression, base, method,
 
     first_input = open_old_stds(input_name_list[0], type, dbif)
 
+    # skip sampling when only one dataset specified (with different
+    # band filters)
+    input_name_list_uniq = []
+    for input_name in input_name_list:
+        ds = open_old_stds(input_name, type, dbif)
+        ds_name = ds.get_name(band_reference=False)
+        if ds_name not in input_name_list_uniq:
+            input_name_list_uniq.append(ds_name)
+    do_sampling = len(input_name_list_uniq) > 1
+
     # All additional inputs in reverse sorted order to avoid
     # wrong name substitution
     input_name_list = input_name_list[1:]
@@ -110,10 +120,11 @@ def dataset_mapcalculator(inputs, output, type, expression, base, method,
     map_matrix = []
     id_list = []
     sample_map_list = []
-    # First entry is the first dataset id
-    id_list.append(first_input.get_name())
 
-    if len(input_list) > 0:
+    if len(input_list) > 0 and do_sampling:
+        # First entry is the first dataset id
+        id_list.append(first_input.get_name())
+
         has_samples = False
         for dataset in input_list:
             list = dataset.sample_by_dataset(stds=first_input,
@@ -163,21 +174,26 @@ def dataset_mapcalculator(inputs, output, type, expression, base, method,
             map_matrix.append(copy.copy(map_name_list))
 
             id_list.append(dataset.get_name())
+
     else:
-        list = first_input.get_registered_maps_as_objects(dbif=dbif)
+        input_list.insert(0, first_input)
+        for dataset in input_list:
+            list = dataset.get_registered_maps_as_objects(dbif=dbif)
 
-        if list is None:
-            dbif.close()
-            msgr.message(_("No maps registered in input dataset"))
-            return 0
+            if list is None:
+                dbif.close()
+                msgr.message(_("No maps registered in input dataset"))
+                return 0
 
-        map_name_list = []
-        for map in list:
-            map_name_list.append(map.get_name())
-            sample_map_list.append(map)
+            map_name_list = []
+            for map in list:
+                map_name_list.append(map.get_name())
+                sample_map_list.append(map)
 
-        # Attach the map names
-        map_matrix.append(copy.copy(map_name_list))
+            # Attach the map names
+            map_matrix.append(copy.copy(map_name_list))
+
+            id_list.append(dataset.get_name())
 
     # Needed for map registration
     map_list = []

+ 38 - 1
lib/python/temporal/metadata.py

@@ -317,22 +317,44 @@ class RasterMetadata(RasterMetadataBase):
     """
     def __init__(self, ident=None, datatype=None,
                  cols=None, rows=None, number_of_cells=None, nsres=None,
-                 ewres=None, min=None, max=None):
+                 ewres=None, min=None, max=None, band_reference=None):
 
         RasterMetadataBase.__init__(self, "raster_metadata", ident, datatype,
                                     cols, rows, number_of_cells, nsres,
                                     ewres, min, max)
 
+        self.set_band_reference(band_reference)
+
+    def set_band_reference(self, band_reference):
+        """Set the band reference identifier"""
+        self.D["band_reference"] = band_reference
+
+    def get_band_reference(self):
+        """Get the band reference identifier
+           :return: None if not found"""
+        if "band_reference" in self.D:
+            return self.D["band_reference"]
+        else:
+            return None
+
+    band_reference = property(fget=get_band_reference, fset=set_band_reference)
+
     def print_info(self):
         """Print information about this class in human readable style"""
         print(" +-------------------- Metadata information ----------------------------------+")
         #      0123456789012345678901234567890
         RasterMetadataBase.print_info(self)
 
+        # band reference section (raster specific only)
+        print(" | Band reference:............. " + str(self.get_band_reference()))
+
     def print_shell_info(self):
         """Print information about this class in shell style"""
         RasterMetadataBase.print_shell_info(self)
 
+        # band reference section (raster specific only)
+        print("band_reference=" + str(self.get_band_reference()))
+
 ###############################################################################
 
 
@@ -1001,6 +1023,7 @@ class STDSRasterMetadataBase(STDSMetadataBase):
              | Maximum value min:.......... None
              | Maximum value max:.......... None
              | Aggregation type:........... None
+             | Number of registered bands:. None
              | Number of registered maps:.. None
              |
              | Title:
@@ -1036,6 +1059,7 @@ class STDSRasterMetadataBase(STDSMetadataBase):
         self.D["ewres_min"] = None
         self.D["ewres_max"] = None
         self.D["aggregation_type"] = aggregation_type
+        self.D["number_of_bands"] = None
 
     def set_aggregation_type(self, aggregation_type):
         """Set the aggregation type of the dataset (mean, min, max, ...)"""
@@ -1130,6 +1154,15 @@ class STDSRasterMetadataBase(STDSMetadataBase):
         else:
             return None
 
+    def get_number_of_bands(self):
+        """Get the number of registered bands
+           :return: None if not found
+        """
+        if "number_of_bands" in self.D:
+            return self.D["number_of_bands"]
+        else:
+            return None
+
     nsres_min = property(fget=get_nsres_min)
     nsres_max = property(fget=get_nsres_max)
     ewres_min = property(fget=get_ewres_min)
@@ -1140,6 +1173,7 @@ class STDSRasterMetadataBase(STDSMetadataBase):
     max_max = property(fget=get_max_max)
     aggregation_type = property(fset=set_aggregation_type,
                                 fget=get_aggregation_type)
+    number_of_bands = property(fget=get_number_of_bands)
 
     def print_info(self):
         """Print information about this class in human readable style"""
@@ -1153,11 +1187,14 @@ class STDSRasterMetadataBase(STDSMetadataBase):
         print(" | Maximum value min:.......... " + str(self.get_max_min()))
         print(" | Maximum value max:.......... " + str(self.get_max_max()))
         print(" | Aggregation type:........... " + str(self.get_aggregation_type()))
+        print(" | Number of registered bands:. " + str(self.get_number_of_bands()))
+
         STDSMetadataBase.print_info(self)
 
     def print_shell_info(self):
         """Print information about this class in shell style"""
         print("aggregation_type=" + str(self.get_aggregation_type()))
+        print("number_of_bands=" + str(self.get_number_of_bands()))
         STDSMetadataBase.print_shell_info(self)
         print("nsres_min=" + str(self.get_nsres_min()))
         print("nsres_max=" + str(self.get_nsres_max()))

+ 16 - 5
lib/python/temporal/open_stds.py

@@ -42,17 +42,25 @@ def open_old_stds(name, type, dbif=None):
        :return: New stds object
 
     """
-    mapset = get_current_mapset()
     msgr = get_tgis_message_interface()
 
-    # Check if the dataset name contains the mapset as well
+    # Check if the dataset name contains the mapset and the band reference as well
     if name.find("@") < 0:
-        id = name + "@" + mapset
+        mapset = get_current_mapset()
     else:
-        id = name
+        name, mapset = name.split('@')
+    band_ref = None
+    if name.find(".") > -1:
+        try:
+            name, band_ref = name.split('.')
+        except ValueError:
+            msgr.fatal("Invalid name of the space time dataset. Only one dot allowed.")
+    id = name + "@" + mapset
 
     if type == "strds" or type == "rast" or type == "raster":
         sp = dataset_factory("strds", id)
+        if band_ref:
+            sp.set_band_reference(band_ref)
     elif type == "str3ds" or type == "raster3d" or type == "rast3d" or type == "raster_3d":
         sp = dataset_factory("str3ds", id)
     elif type == "stvds" or type == "vect" or type == "vector":
@@ -67,7 +75,6 @@ def open_old_stds(name, type, dbif=None):
         msgr.fatal(_("Space time %(sp)s dataset <%(name)s> not found") %
                    {'sp': sp.get_new_map_instance(None).get_type(),
                     'name': name})
-
     # Read content from temporal database
     sp.select(dbif)
     if connected:
@@ -108,6 +115,10 @@ def check_new_stds(name, type, dbif=None, overwrite=False):
         id = name
 
     if type == "strds" or type == "rast" or type == "raster":
+        if name.find('.') > -1:
+            # a dot is used as a separator for band reference filtering
+            msgr.fatal(_("Illegal dataset name <{}>. "
+                         "Character '.' not allowed.").format(name))
         sp = dataset_factory("strds", id)
     elif type == "str3ds" or type == "raster3d" or type == "rast3d "or type == "raster_3d":
         sp = dataset_factory("str3ds", id)

+ 36 - 1
lib/python/temporal/register.py

@@ -66,6 +66,8 @@ def register_maps_in_space_time_dataset(
     """
     start_time_in_file = False
     end_time_in_file = False
+    band_reference_in_file = False
+
     msgr = get_tgis_message_interface()
 
     # Make sure the arguments are of type string
@@ -143,16 +145,29 @@ def register_maps_in_space_time_dataset(
 
             line_list = line.split(fs)
 
-            # Detect start and end time
+            # Detect start and end time (and band reference)
             if len(line_list) == 2:
                 start_time_in_file = True
                 end_time_in_file = False
+                band_reference_in_file = False
             elif len(line_list) == 3:
                 start_time_in_file = True
+                # Check if last column is an end time or a band reference
+                time_object = check_datetime_string(line_list[2])
+                if not sp.is_time_relative() and isinstance(time_object, datetime):
+                    end_time_in_file = True
+                    band_reference_in_file = False
+                else:
+                    end_time_in_file = False
+                    band_reference_in_file = True
+            elif len(line_list) == 4:
+                start_time_in_file = True
                 end_time_in_file = True
+                band_reference_in_file = True
             else:
                 start_time_in_file = False
                 end_time_in_file = False
+                band_reference_in_file = False
 
             mapname = line_list[0].strip()
             row = {}
@@ -164,6 +179,10 @@ def register_maps_in_space_time_dataset(
             if start_time_in_file and not end_time_in_file:
                 row["start"] = line_list[1].strip()
 
+            if band_reference_in_file:
+                idx = 3 if end_time_in_file else 2
+                row["band_reference"] = line_list[idx].strip().upper() # case-insensitive
+
             row["id"] = AbstractMapDataset.build_id(mapname, mapset)
 
             maplist.append(row)
@@ -202,6 +221,12 @@ def register_maps_in_space_time_dataset(
         if "end" in maplist[count]:
             end = maplist[count]["end"]
 
+        # Use the band reference from file
+        if "band_reference" in maplist[count]:
+            band_reference = maplist[count]["band_reference"]
+        else:
+            band_reference = None
+
         is_in_db = False
 
         # Put the map into the database
@@ -303,6 +328,16 @@ def register_maps_in_space_time_dataset(
                                      increment=increment, mult=count,
                                      interval=interval)
 
+        # Set the band reference
+        if band_reference:
+            # Band reference defined in input file
+            # -> update raster metadata
+            # -> write band identifier to GRASS data base
+            map.set_band_reference(band_reference)
+        else:
+            # Try to read band reference from GRASS data base if defined
+            map.read_band_reference_from_grass()
+
         if is_in_db:
             #  Gather the SQL update statement
             statement += map.update_all(dbif=dbif, execute=False)

+ 65 - 0
lib/python/temporal/space_time_datasets.py

@@ -303,6 +303,45 @@ class RasterDataset(AbstractMapDataset):
 
         return True
 
+    def read_band_reference_from_grass(self):
+        """Read the band identifier of this map from the map metadata
+           in the GRASS file system based spatial database and
+           set the internal band identifier that should be insert/updated
+           in the temporal database.
+
+           :return: True if success, False on error
+        """
+
+        check, band_ref = self.ciface.read_raster_band_reference(self.get_name(),
+                                                                 self.get_mapset())
+
+        if check < 1:
+            self.msgr.error(_("Unable to read band reference file "
+                              "for raster map <%s>" % (self.get_map_id())))
+            return False
+
+        self.metadata.set_band_reference(band_ref)
+
+        return True
+
+    def write_band_reference_to_grass(self):
+        """Write the band identifier of this map into the map metadata in
+           the GRASS file system based spatial database.
+
+           Internally the libgis API functions are used for writing
+
+           :return: True if success, False on error
+        """
+        check = self.ciface.write_raster_band_reference(self.get_name(),
+                                                        self.get_mapset(),
+                                                        self.metadata.get_band_reference())
+        if check == -1:
+            self.msgr.error(_("Unable to write band identifier for raster map <%s>"
+                            % (self.get_name())))
+            return False
+
+        return True
+
     def map_exists(self):
         """Return True in case the map exists in the grass spatial database
 
@@ -354,10 +393,29 @@ class RasterDataset(AbstractMapDataset):
             self.metadata.set_rows(rows)
             self.metadata.set_number_of_cells(ncells)
 
+            # Fill band reference if defined
+            check, band_ref = self.ciface.read_raster_band_reference(self.get_name(),
+                                                                     self.get_mapset())
+            if check > 0:
+                self.metadata.set_band_reference(band_ref)
+
             return True
 
         return False
 
+    def set_band_reference(self, band_reference):
+        """Set band reference identifier
+
+        Metadata is updated in order to propagate band identifier into
+        temporal DB.
+
+        File-based band identifier stored in GRASS data base.
+
+        :param str band_reference: band reference identifier (eg. S2_1)
+        """
+        self.metadata.set_band_reference(band_reference)
+        self.write_band_reference_to_grass()
+
 ###############################################################################
 
 
@@ -1048,6 +1106,13 @@ class SpaceTimeRasterDataset(AbstractSpaceTimeDataset):
     def __init__(self, ident):
         AbstractSpaceTimeDataset.__init__(self, ident)
 
+    def set_band_reference(self, band_reference):
+        """Set band reference identifier
+
+        :param str band_reference: band reference identifier (eg. S2_1)
+        """
+        self.band_reference = band_reference
+
     def is_stds(self):
         """Return True if this class is a space time dataset
 

+ 126 - 0
lib/raster/band_reference.c

@@ -0,0 +1,126 @@
+/*!
+ * \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);
+}

+ 1 - 0
lib/temporal/SQL/raster_metadata_table.sql

@@ -19,6 +19,7 @@ CREATE TABLE  raster_metadata (
   ewres DOUBLE PRECISION NOT NULL,
   min DOUBLE PRECISION,
   max DOUBLE PRECISION,
+  band_reference VARCHAR,
   PRIMARY KEY (id)
 );
 

+ 2 - 2
lib/temporal/SQL/raster_views.sql

@@ -16,7 +16,7 @@ CREATE VIEW raster_view_abs_time AS SELECT
             A3.north, A3.south, A3.east, A3.west, A3.bottom, A3.top, A3.proj,
             A4.datatype, A4.cols, A4.rows,
             A4.nsres, A4.ewres, A4.min, A4.max,
-            A4.number_of_cells, A5.registered_stds
+            A4.number_of_cells, A4.band_reference, A5.registered_stds
             FROM raster_base A1, raster_absolute_time A2, 
             raster_spatial_extent A3, raster_metadata A4,
             raster_stds_register A5
@@ -32,7 +32,7 @@ CREATE VIEW raster_view_rel_time AS SELECT
             A3.north, A3.south, A3.east, A3.west, A3.bottom, A3.top, A3.proj,
             A4.datatype, A4.cols, A4.rows,
             A4.nsres, A4.ewres, A4.min, A4.max,
-            A4.number_of_cells, A5.registered_stds
+            A4.number_of_cells, A4.band_reference, A5.registered_stds
             FROM raster_base A1, raster_relative_time A2, 
             raster_spatial_extent A3, raster_metadata A4,
             raster_stds_register A5

+ 1 - 0
lib/temporal/SQL/strds_metadata_table.sql

@@ -18,6 +18,7 @@ CREATE TABLE  strds_metadata (
   ewres_min DOUBLE PRECISION,   -- The lowest east-west resolution of the registered raster maps
   ewres_max DOUBLE PRECISION,   -- The highest east-west resolution of the registered raster maps
   aggregation_type VARCHAR,     -- The aggregation type of the dataset (mean, min, max, ...) set by aggregation modules
+  number_of_bands INTEGER,       -- The number of registered bands
   title VARCHAR,                -- Title of the space-time raster dataset
   description VARCHAR,          -- Detailed description of the space-time raster dataset
   command VARCHAR,              -- The command that was used to create the space time raster dataset

+ 6 - 0
lib/temporal/SQL/update_strds_metadata_template.sql

@@ -41,3 +41,9 @@ UPDATE strds_metadata SET ewres_max =
        (SELECT max(ewres) FROM raster_metadata WHERE raster_metadata.id IN 
     		(SELECT id FROM SPACETIME_REGISTER_TABLE)
        ) WHERE id = 'SPACETIME_ID';
+-- Update the number of registered bands
+UPDATE strds_metadata SET number_of_bands =
+       (SELECT count(distinct band_reference) FROM raster_metadata WHERE
+       raster_metadata.id IN
+    		(SELECT id FROM SPACETIME_REGISTER_TABLE)
+       ) WHERE id = 'SPACETIME_ID';

+ 2 - 0
scripts/Makefile

@@ -18,11 +18,13 @@ SUBDIRS = \
 	db.out.ogr \
 	db.test \
 	db.univar \
+	g.bands \
 	g.extension \
 	g.extension.all \
 	g.manual \
 	g.search.modules \
 	i.colors.enhance \
+	i.band \
 	i.image.mosaic \
 	i.in.spotvgt \
 	i.oif \

+ 18 - 0
scripts/g.bands/Makefile

@@ -0,0 +1,18 @@
+MODULE_TOPDIR = ../..
+
+PGM = g.bands
+
+# TODO: unfortunately Script.make assumes that ETCFILES are Python
+# modules only, this should be improved (by introducing a new variable
+# in Script.make), there are more affected modules, see eg. d.polar or
+# db.tests
+JSON_FLS = $(wildcard *.json)
+JSON_ETC = $(patsubst %,$(ETC)/$(PGM)/%,$(JSON_FLS))
+
+include $(MODULE_TOPDIR)/include/Make/Script.make
+include $(MODULE_TOPDIR)/include/Make/Python.make
+
+default: script $(JSON_ETC)
+
+$(ETC)/$(PGM)/%: % | $(ETC)/$(PGM)
+	$(INSTALL_DATA) $< $@

+ 214 - 0
scripts/g.bands/g.bands.html

@@ -0,0 +1,214 @@
+<h2>DESCRIPTION</h2>
+
+Prints available band references information of multispectral data
+defined by GRASS GIS.
+
+<p>
+Band references to be printed can be filtered by a search pattern (or
+fully defined band reference identifier) which can be specified
+by <b>pattern</b> option. For pattern syntax
+see <a href="https://docs.python.org/3/library/re.html#regular-expression-syntax">Python
+regular expression operations</a> documentation. By
+default, <em>g.bands</em> prints all available band references.
+
+<p>
+Extended metadata is printed only when <b>-e</b> flag is given.
+
+<h2>NOTES</h2>
+
+Specific band reference can be assigned to a raster map
+by <em><a href="i.band.html">i.band</a></em> module.
+
+<p>
+Band reference concept is supported by temporal GRASS modules,
+see <em><a href="t.register.html#support-for-band-references">t.register</a></em>,
+<em><a href="t.rast.list.html#filtering-the-result-by-band-references">t.rast.list</a></em>,
+<em><a href="t.info.html#space-time-dataset-with-band-references-assigned">t.info</a></em>
+and <em><a href="t.rast.mapcalc.html#band-reference-filtering">t.rast.mapcalc</a></em>
+modules for examples.
+
+<h3>Image collections</h3>
+
+Image collections are the common data type to reference time series of
+multi band data. It is used in many frameworks
+(see <a href="https://developers.google.com/earth-engine/tutorial_api_04">Google
+Earth Engine API</a> for example) to address multi spectral satellite
+images series. GRASS supports a multi-band raster layer approach
+basically with the imagery group concept
+(<em><a href="i.group.html">i.group</a></em>). A new band reference
+concept is designed in order to support image collections in GRASS
+GIS.
+
+<h3>Band reference registry files</h3>
+
+Band reference information is stored in JSON files with a pre-defined
+internal data structure. A minimalistic example is shown below.
+
+<div class="code"><pre>
+{
+    "Sentinel2": {
+        "description": "The Sentinel-2 A/B bands",
+        "shortcut": "S2",
+        "instruments": "MultiSpectral Instrument (MSI) optical and infrared",
+        "launched": "23 June 2015 (A); 07 March 2017 (B)",
+        "source": "https://sentinel.esa.int/web/sentinel/missions/sentinel-2",
+        "bands": {
+            "1": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 443.9,
+                    "bandwidth (nm)": 27
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 442.3,
+                    "bandwidth (nm)": 45
+                },
+                "spatial resolution (meters)": 60,
+                "tag": "Visible (Coastal/Aerosol)"
+            },
+            "2": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 496.6,
+                    "bandwidth (nm)": 98
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 492.1,
+                    "bandwidth (nm)": 98
+                },
+                "spatial resolution (meters)": 10,
+                "tag": "Visible (Blue)"
+            }
+        }
+    }
+}
+</pre></div>
+
+Each series starts with an unique identifier (&quot;Sentinel2&quot;
+in example above). Required attributes are only two:
+a <tt>shortcut</tt> and <tt>bands</tt>. Note that a <i>shortcut</i>
+must be unique in all band reference registry files. Number of other
+attributes is not defined or even limited (in example above
+only <tt>description</tt> and <tt>instruments</tt> attributes are
+defined). List of bands is defined by a <tt>bands</tt> attribute. Each
+band is defined by an identifier ("1", "2" in example above). List of
+attributes describing each band is not pre-defined or limited. In
+example above each band is described by a <tt>central wavelength
+(nm)</tt>, <tt>bandwidth (nm)</tt>, and a <tt>tag</tt>.
+
+<p>
+Band reference identifier defined by <b>pattern</b> option is given by
+a <i>shortcut</i> in order to print band reference information for
+whole series or by specific <i>shortcut</i>_<i>band</i>
+identifier.
+
+<p>
+System-defined registry files are located in GRASS GIS installation
+directory (<tt>$GISBASE/etc/g.bands</tt>). Note that
+currently <i>g.bands</i> allows to manage only system-defined registry
+files. Support for user-defined registry files is planned to be
+implemented, see <a href="#known-issues">KNOWN ISSUES</a> section for
+details.
+
+<h2>EXAMPLES</h2>
+
+<h3>Print all available band references</h3>
+
+<div class="code"><pre>
+g.bands
+
+S2_1 Visible (Coastal/Aerosol)
+S2_2 Visible (Blue)
+...
+L7_1 Visible (Blue)
+L7_2 Visible (Green)
+...
+L8_1 Visible (Coastal/Aerosol)
+L8_2 Visible (Blue)
+...
+</pre></div>
+
+The module prints band reference and related tag if defined.
+
+<h3>Filter band references by a shortcut</h3>
+
+Only band identifiers related to Sentinel-2 satellite will be
+printed.
+
+<div class="code"><pre>
+g.bands pattern=S2
+
+S2_1 Visible (Coastal/Aerosol)
+...
+S2_12 SWIR 2
+</pre></div>
+
+<h3>Filter band references by a regular expression</h3>
+
+Print all available 2nd bands:
+
+<div class="code"><pre>
+g.bands pattern=.*_2
+
+S2_2 Visible (Blue)
+L7_2 Visible (Green)
+L8_2 Visible (Blue)
+...
+</pre></div>
+
+<h3>Print extended metadata for specified band identifier</h3>
+
+Extended metadata related to the first band of Sentinel-2 satellite
+will be printed.
+
+<div class="code"><pre>
+g.bands -e pattern=S2_1
+
+description: The Sentinel-2 A/B bands
+shortcut: S2
+instruments: MultiSpectral Instrument (MSI) optical and infrared
+launched: 23 June 2015 (A); 07 March 2017 (B)
+source: https://sentinel.esa.int/web/sentinel/missions/sentinel-2
+    band: 1
+        Sentinel 2A:
+                central wavelength (nm): 443.9
+                bandwidth (nm): 27
+        Sentinel 2B:
+                central wavelength (nm): 442.3
+                bandwidth (nm): 45
+        spatial resolution (meters): 60
+        tag: Visible (Coastal/Aerosol)
+</pre></div>
+
+<h2>KNOWN ISSUES</h2>
+
+<em>g.bands</em> has currently <b>very limited functionality</b>. Only
+system-defined band references are supported. The final implementation
+will support managing (add, modify, delete) user-defined band
+references.
+
+<p>
+Only very limited number of band references is currently
+defined, namely Sentinel-2, Landsat7, and Landsat8 satellites. This
+will be improved in the near future.
+
+<h2>REFERENCES</h2>
+
+<ul>
+  <li><a href="https://developers.google.com/earth-engine/tutorial_api_04">Google Earth Engine API</a></li>
+</ul>
+
+<h2>SEE ALSO</h2>
+
+<em>
+  <a href="i.band.html">i.band</a>,
+  <a href="r.info.html">r.info</a>
+</em>
+
+<h2>AUTHORS</h2>
+
+Martin Landa<br>
+Development sponsored by <a href="https://www.mundialis.de/en">mundialis
+GmbH &amp; Co. KG</a> (for the <a href="https://openeo.org">openEO</a>
+EU H2020 grant 776242)
+
+<p>
+<i>Last changed: $Date$</i>

+ 79 - 0
scripts/g.bands/g.bands.py

@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+
+############################################################################
+#
+# MODULE:       g.bands
+# AUTHOR(S):    Martin Landa <landa.martin gmail com>
+#
+# PURPOSE:      Prints available band reference information used for multispectral data.
+#
+# COPYRIGHT:    (C) 2019 by mundialis GmbH & Co.KG, 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.
+#
+#############################################################################
+
+#%module
+#% description: Prints available band reference information used for multispectral data.
+#% keyword: general
+#% keyword: imagery
+#% keyword: band reference
+#% keyword: image collections
+#%end
+#%option
+#% key: pattern
+#% type: string
+#% description: Band reference search pattern (examples: L, S2, .*_2, S2_1)
+#% required: no
+#% multiple: no
+#%end
+#%option
+#% key: operation
+#% type: string
+#% required: no
+#% multiple: no
+#% options: print
+#% description: Operation to be performed
+#% answer: print
+#%end
+#%flag
+#% key: e
+#% description: Print extended metadata information
+#%end
+
+import sys
+
+import grass.script as gs
+
+def main():
+    from grass.bandref import BandReferenceReader, BandReferenceReaderError
+
+    band = None
+    kwargs = {}
+    if ',' in options['pattern']:
+        gs.fatal("Multiple values not supported")
+    if '_' in options['pattern']:
+        # full band identifier specified
+        kwargs['shortcut'], kwargs['band'] = options['pattern'].split('_')
+    else:
+        # pattern
+        kwargs['shortcut'] = options['pattern']
+    kwargs['extended'] = flags['e']
+
+    if options['operation'] == 'print':
+        try:
+            reader = BandReferenceReader()
+            reader.print_info(**kwargs)
+        except BandReferenceReaderError as e:
+            gs.fatal(e)
+
+    return 0
+
+if __name__ == "__main__":
+    options, flags = gs.parser()
+
+    sys.exit(
+        main()
+    )

+ 159 - 0
scripts/g.bands/landsat.json

@@ -0,0 +1,159 @@
+{
+    "Landsat5": {
+        "description": "The Landsat 5 bands",
+        "shortcut": "L5",
+        "instruments": "Thematic Mapper (TM) optical and infrared",
+        "launched": "March 1, 1984",
+        "source": "https://www.usgs.gov/land-resources/nli/landsat/landsat-5",
+        "bands": {
+            "1": {
+                "wavelength (µm)": "0.45-0.52",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Blue)"
+            },
+            "2": {
+                "wavelength (µm)": "0.52-0.60",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Green)"
+            },
+            "3": {
+                "wavelength (µm)": "0.63-0.69",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Red)"
+            },
+            "4": {
+                "wavelength (µm)": "0.76-0.90",
+                "spatial resolution (meters)": 30,
+                "tag": "Near-Infrared"
+            },
+            "5": {
+                "wavelength (µm)": "1.55-1.75",
+                "spatial resolution (meters)": 30,
+                "tag": "Short-wave infrared"
+            },
+            "6": {
+                "wavelength (µm)": "10.40-12.50",
+                "spatial resolution (meters)": 60,
+                "tag": "Thermal"
+            },
+            "7": {
+                "wavelength (µm)": "2.08-2.35",
+                "spatial resolution (meters)": 30,
+                "tag": "Short-wave infrared"
+            }
+        }
+    },
+    "Landsat7": {
+        "description": "The Landsat 7 bands",
+        "shortcut": "L7",
+        "instruments": "Enhanced Thematic Mapper (ETM+) optical and infrared",
+        "launched": "April 15, 1999",
+        "source": "https://www.usgs.gov/land-resources/nli/landsat/landsat-7",
+        "bands": {
+            "1": {
+                "wavelength (µm)": "0.45-0.52",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Blue)"
+            },
+            "2": {
+                "wavelength (µm)": "0.52-0.60",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Green)"
+            },
+            "3": {
+                "wavelength (µm)": "0.63-0.69",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Red)"
+            },
+            "4": {
+                "wavelength (µm)": "0.77-0.90",
+                "spatial resolution (meters)": 30,
+                "tag": "Near-Infrared"
+            },
+            "5": {
+                "wavelength (µm)": "1.55-1.75",
+                "spatial resolution (meters)": 30,
+                "tag": "Near-Infrared"
+            },
+            "6": {
+                "wavelength (µm)": "10.40-12.50",
+                "spatial resolution (meters)": 60,
+                "tag": "Thermal"
+            },
+            "7": {
+                "wavelength (µm)": "2.09-2.35",
+                "spatial resolution (meters)": 30,
+                "tag": "Mid-Infrared"
+            },
+            "8": {
+                "wavelength (µm)": "52-90",
+                "spatial resolution (meters)": 15,
+                "tag": "Panchromatic"
+            }
+        }
+    },
+    "Landsat8": {
+        "description": "The Landsat 8 bands",
+        "shortcut": "L8",
+        "instruments": "Operational Land Imager (OLI) and the Thermal Infrared Sensor (TIRS)",
+        "launched": "February 11, 2013",
+        "source": "https://www.usgs.gov/land-resources/nli/landsat/landsat-8",
+        "bands": {
+            "1": {
+                "wavelength (µm)": "0.43-0.45",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Coastal/Aerosol)"
+            },
+            "2": {
+                "wavelength (µm)": "0.45-0.51",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Blue)"
+            },
+            "3": {
+                "wavelength (µm)": "0.53-0.59",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Green)"
+            },
+            "4": {
+                "wavelength (µm)": "0.64-0.67",
+                "spatial resolution (meters)": 30,
+                "tag": "Visible (Red)"
+            },
+            "5": {
+                "wavelength (µm)": "0.85-0.88",
+                "spatial resolution (meters)": 30,
+                "tag": "Near-Infrared"
+            },
+            "6": {
+                "wavelength (µm)": "1.57-1.65",
+                "spatial resolution (meters)": 30,
+                "tag": "SWIR 1"
+            },
+            "7": {
+                "wavelength (µm)": "2.11-2.29",
+                "spatial resolution (meters)": 30,
+                "tag": "SWIR 2"
+            },
+            "8": {
+                "wavelength (µm)": "0.50-0.68",
+                "spatial resolution (meters)": 15,
+                "tag": "Panchromatic"
+            },
+            "9": {
+                "wavelength (µm)": "1.36-1.38",
+                "spatial resolution (meters)": 30,
+                "tag": "Cirrus"
+             },
+            "10": {
+                "wavelength (µm)": "10.6-11.19",
+                "spatial resolution (meters)": 100,
+                "tag": "TIRS 1"
+            },
+            "11": {
+                "wavelength (µm)": "11.5-12.51",
+                "spatial resolution (meters)": 100,
+                "tag": "TIRS 1"
+            }
+        }
+    }
+}

+ 167 - 0
scripts/g.bands/sentinel.json

@@ -0,0 +1,167 @@
+{
+    "Sentinel2": {
+        "description": "The Sentinel-2 A/B bands",
+        "shortcut": "S2",
+        "instruments": "MultiSpectral Instrument (MSI) optical and infrared",
+        "launched": "23 June 2015 (A); 07 March 2017 (B)",
+        "source": "https://sentinel.esa.int/web/sentinel/missions/sentinel-2",
+        "bands": {
+            "1": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 443.9,
+                    "bandwidth (nm)": 27
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 442.3,
+                    "bandwidth (nm)": 45
+                },
+                "spatial resolution (meters)": 60,
+                "tag": "Visible (Coastal/Aerosol)"
+            },
+            "2": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 496.6,
+                    "bandwidth (nm)": 98
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 492.1,
+                    "bandwidth (nm)": 98
+                },
+                "spatial resolution (meters)": 10,
+                "tag": "Visible (Blue)"
+            },
+            "3": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 560.0,
+                    "bandwidth (nm)": 45
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 559.0,
+                    "bandwidth (nm)": 46
+                },
+                "spatial resolution (meters)": 10,
+                "tag": "Visible (Green)"
+            },
+            "4": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 664.5,
+                    "bandwidth (nm)": 38
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 665.0,
+                    "bandwidth (nm)": 39
+                },
+                "spatial resolution (meters)": 10,
+                "tag": "Visible (Red)"
+            },
+            "5": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 703.9,
+                    "bandwidth (nm)": 19
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 703.8,
+                    "bandwidth (nm)": 20
+                },
+                "spatial resolution (meters)": 20,
+                "tag": "Vegetation Red Edge 1"
+            },
+            "6": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 740.2,
+                    "bandwidth (nm)": 18
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 739.1,
+                    "bandwidth (nm)": 18
+                },
+                "spatial resolution (meters)": 20,
+                "tag": "Vegetation Red Edge 2"
+            },
+            "7": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 782.5,
+                    "bandwidth (nm)": 28
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 779.7,
+                    "bandwidth (nm)": 28
+                },
+                "spatial resolution (meters)": 20,
+                "tag": "Vegetation Red Edge 3"
+            },
+            "8": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 835.1,
+                    "bandwidth (nm)": 145
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 833.0,
+                    "bandwidth (nm)": 133
+                },
+                "spatial resolution (meters)": 10,
+                "tag": "Near-Infrared"
+            },
+            "8A": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 864.8,
+                    "bandwidth (nm)": 33
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 864.0,
+                    "bandwidth (nm)": 32
+                },
+                "spatial resolution (meters)": 20,
+                "tag": "Narrow Near-Infrared"
+            },
+            "9": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 945.0,
+                    "bandwidth (nm)": 26
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 943.2,
+                    "bandwidth (nm)": 27
+                },
+                "spatial resolution (meters)": 60,
+                "tag": "Water vapour"
+            },
+            "10": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 1373.5,
+                    "bandwidth (nm)": 75
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 1376.9,
+                    "bandwidth (nm)": 76
+                },
+                "spatial resolution (meters)": 60,
+                "tag": "SWIR - Cirrus"
+            },
+            "11": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 1613.7,
+                    "bandwidth (nm)": 143
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 1610.4,
+                    "bandwidth (nm)": 141
+                },
+                "spatial resolution (meters)": 20,
+                "tag": "SWIR 1"
+            },
+            "12": {
+                "Sentinel 2A" : {
+                    "central wavelength (nm)": 2202.4,
+                    "bandwidth (nm)": 242
+                },
+                "Sentinel 2B" : {
+                    "central wavelength (nm)": 2185.7,
+                    "bandwidth (nm)": 238
+                },
+                "spatial resolution (meters)": 20,
+                "tag": "SWIR 2"
+            }
+        }
+    }
+}

+ 42 - 0
scripts/g.bands/testsuite/test_g_bands.py

@@ -0,0 +1,42 @@
+import os
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+from grass.gunittest.gmodules import call_module
+
+class TestBandsSystemDefined(TestCase):
+    @staticmethod
+    def _number_of_bands(**kwargs):
+        gbands = call_module('g.bands', **kwargs)
+        return len(gbands.rstrip(os.linesep).split(os.linesep))
+
+    def test_number_system_defined(self):
+        # test number of valid band identifiers
+        #
+        # get number of valid band identifiers by g.bands
+        nbands = self._number_of_bands()
+
+        # get number of valid band identifiers by Bands lib
+        from grass.bandref import BandReferenceReader
+        nbands_ref = len(BandReferenceReader().get_bands())
+
+        self.assertEqual(nbands, nbands_ref)
+
+    def test_number_s2(self):
+        # test number of S2 band identifiers (hardcoded, no changes expected)
+        #
+        nbands = self._number_of_bands(band='S2')
+
+        self.assertEqual(nbands, 13)
+
+
+    def test_number_s2_1(self):
+        # test if S2_1 is defined (lower + upper case)
+        #
+        band = 'S2_1'
+        for iband in [band, band.lower()]:
+            nbands = self._number_of_bands(band=iband)
+            self.assertEqual(nbands, 1)
+
+if __name__ == '__main__':
+    test()

+ 7 - 0
scripts/i.band/Makefile

@@ -0,0 +1,7 @@
+MODULE_TOPDIR = ../..
+
+PGM = i.band
+
+include $(MODULE_TOPDIR)/include/Make/Script.make
+
+default: script

+ 89 - 0
scripts/i.band/i.band.html

@@ -0,0 +1,89 @@
+<h2>DESCRIPTION</h2>
+
+<em>i.band</em> allows assigning a band reference information to a
+single raster map or to a list of specified raster maps. Band
+reference can be defined by <b>band</b> option. Already assigned band
+reference can be removed from a specified raster map
+by <b>operation=remove</b>. The module also allows printing detailed
+band reference information already assigned to a raster map
+by <b>operation=print</b>.
+
+<p>
+Either a single raster map or a list of raster maps can be given
+by <b>map</b> option.
+
+<h2>NOTES</h2>
+
+Note that <i>only raster maps from the current mapsets</i> can be modified.
+
+<p>
+For more information about band reference concept
+see <em><a href="g.bands.html">g.bands</a></em> module.
+
+<p>Band references are supported by temporal GRASS modules. Name of
+STRDS can be extended by band identifier in order to filter the result
+by a band reference. See
+<em><a href="t.register.html#support-for-band-references">t.register</a></em>,
+<em><a href="t.rast.list.html#filtering-the-result-by-band-references">t.rast.list</a></em>, <em><a href="t.info.html#space-time-dataset-with-band-references-assigned">t.info</a></em>
+and <em><a href="t.rast.mapcalc.html#band-reference-filtering">t.rast.mapcalc</a></em>
+modules for examples.
+
+<h2>EXAMPLES</h2>
+
+<h3>Assign band reference to a single raster map</h3>
+
+<div class="code"><pre>
+i.band map=T33UVR_20180506T100031_B01 band=S2_1
+</pre></div>
+
+<h3>Assign band reference to a list of raster maps</h3>
+
+<div class="code"><pre>
+i.band map=T33UVR_20180506T100031_B01,T33UVR_20180521T100029_B01 band=S2_1,S2_1
+</pre></div>
+
+<h3>Assign different band references to a list of raster maps</h3>
+
+<div class="code"><pre>
+i.band map=T33UVR_20180506T100031_B01,T33UVR_20180506T100031_B02 band=S2_1,S2_2
+</pre></div>
+
+<h3>Remove band reference from a list of raster maps</h3>
+
+<div class="code"><pre>
+i.band map=T33UVR_20180506T100031_B01,T33UVR_20180506T100031_B02 operation=remove
+</pre></div>
+
+<h3>Print band reference information about single raster map</h3>
+
+<div class="code"><pre>
+i.band map=T33UVR_20180506T100031_B01 operation=print
+</pre></div>
+
+<h3>Print extended band reference information for a list of raster map</h3>
+
+<div class="code"><pre>
+i.band map=T33UVR_20180506T100031_B01,T33UVR_20180506T100031_B02 operation=print
+</pre></div>
+
+<h2>KNOWN ISSUES</h2>
+
+<em>i.band</em> allows managing band references only related to 2D
+raster maps.
+
+<h2>SEE ALSO</h2>
+
+<em>
+  <a href="g.bands.html">g.bands</a>,
+  <a href="r.info.html">r.info</a>
+</em>
+
+<h2>AUTHORS</h2>
+
+Martin Landa<br>
+Development sponsored by <a href="https://www.mundialis.de/en">mundialis
+GmbH &amp; Co. KG</a> (for the <a href="https://openeo.org">openEO</a>
+EU H2020 grant 776242)
+
+<p>
+<i>Last changed: $Date$</i>

+ 129 - 0
scripts/i.band/i.band.py

@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+
+############################################################################
+#
+# MODULE:       i.band
+# AUTHOR(S):    Martin Landa <landa.martin gmail com>
+#
+# PURPOSE:      Manages band reference information assigned to a single
+#               raster map or to a list of raster maps.
+#
+# COPYRIGHT:    (C) 2019 by mundialis GmbH & Co.KG, 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.
+#
+#############################################################################
+
+#%module
+#% description: Manages band reference information assigned to a single raster map or to a list of raster maps.
+#% keyword: general
+#% keyword: imagery
+#% keyword: band reference
+#% keyword: image collections
+#%end
+#%option G_OPT_R_MAPS
+#%end
+#%option
+#% key: band
+#% type: string
+#% key_desc: name
+#% description: Name of band reference identifier (example: S2_1)
+#% required: no
+#% multiple: yes
+#%end
+#%option
+#% key: operation
+#% type: string
+#% required: yes
+#% multiple: no
+#% options: add,remove,print
+#% description: Operation to be performed
+#% answer: add
+
+import sys
+
+import grass.script as gs
+from grass.pygrass.raster import RasterRow
+from grass.exceptions import GrassError, OpenError
+
+def print_map_band_reference(name, band_reader):
+    """Print band reference information assigned to a single raster map
+
+    :param str name: raster map name
+    """
+    try:
+        with RasterRow(name) as rast:
+            band_ref = rast.info.band_reference
+            if band_ref:
+                shortcut, band = band_ref.split('_')
+                band_reader.print_info(shortcut, band)
+            else:
+                gs.info(_("No band reference assigned to <{}>").format(name))
+    except OpenError as e:
+        gs.error(_("Map <{}> not found").format(name))
+
+def manage_map_band_reference(name, band_ref):
+    """Manage band reference assigned to a single raster map
+
+    :param str name: raster map name
+    :param str band_ref: band reference (None for dissociating band reference)
+
+    :return int: return code
+    """
+    try:
+        with RasterRow(name) as rast:
+            if band_ref:
+                gs.debug(_("Band reference <{}> assigned to raster map <{}>").format(
+                    band_ref, name), 1)
+            else:
+                gs.debug(_("Band reference dissociated from raster map <{}>").format(
+                    name), 1)
+            try:
+                rast.info.band_reference = band_ref
+            except GrassError as e:
+                gs.error(_("Unable to assign/dissociate band reference. {}").format(e))
+                return 1
+    except OpenError as e:
+        gs.error(_("Map <{}> not found in current mapset").format(name))
+        return 1
+
+    return 0
+
+def main():
+    maps = options['map'].split(',')
+    if options['operation'] == 'add':
+        if not options['band']:
+            gs.fatal(_("Operation {}: required parameter <{}> not set").format(
+                options['operation'], 'band')
+            )
+        bands = options['band'].split(',')
+        if len(bands) > 1 and len(bands) != len(maps):
+            gs.fatal(_("Number of maps differs from number of bands"))
+    else:
+        bands = [None]
+
+    if options['operation'] == 'print':
+        from grass.bandref import BandReferenceReader
+        band_reader = BandReferenceReader()
+    else:
+        band_reader = None
+    multi_bands = len(bands) > 1
+    ret = 0
+    for i in range(len(maps)):
+        band_ref = bands[i] if multi_bands else bands[0]
+        if options['operation'] == 'print':
+            print_map_band_reference(maps[i], band_reader)
+        else:
+            if manage_map_band_reference(maps[i], band_ref) != 0:
+                ret = 1
+
+    return ret
+
+if __name__ == "__main__":
+    options, flags = gs.parser()
+
+    sys.exit(
+        main()
+    )

+ 44 - 0
scripts/i.band/testsuite/test_i_band.py

@@ -0,0 +1,44 @@
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+from grass.gunittest.gmodules import SimpleModule, call_module
+
+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"
+
+    def read_band_ref(self):
+        with RasterRow(self.raster_map) as rast:
+            band_ref = rast.info.band_reference
+
+        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)
+
+    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)
+        self.assertModule(module)
+
+        # check also using pygrass
+        self.assertEqual(self.read_band_ref(), self.band_ref)
+
+    def test_band_ref_dissociate(self):
+        module = SimpleModule('i.band', flags='r', map=self.raster_map)
+        self.assertModule(module)
+
+        # check also using pygrass
+        self.assertEqual(self.read_band_ref(), None)
+
+if __name__ == '__main__':
+    test()

+ 25 - 0
temporal/t.info/t.info.html

@@ -137,6 +137,31 @@ t.info input=2009_01_tempmean type=raster
  +----------------------------------------------------------------------------+
 </pre></div>
 
+<h3>Space time dataset with band references assigned</h3>
+
+This information is printed only when band references have been assigned
+to registered raster maps by <em><a href="i.band.html">i.band</a></em>
+or <em><a href="t.register.html#support-for-band-references">t.register</a></em> module.
+
+<div class="code"><pre>
+t.info input=test
+...
++-------------------- Metadata information ----------------------------------+
+...
+ | Number of registered bands:. 13
+...
+</pre></div>
+
+Similarly for temporal maps information:
+
+<div class="code"><pre>
+t.info input=T33UYP_20190331T094039_B01 type=raster
+...
+ +-------------------- Metadata information ----------------------------------+
+ | Band reference:............. S2_1
+...
+</pre></div>
+
 <h2>SEE ALSO</h2>
 
 <em>

+ 26 - 0
temporal/t.rast.list/t.rast.list.html

@@ -184,6 +184,32 @@ id|name|mapset|start_time|end_time|interval_length|distance_from_begin
 For the <em>deltagaps</em> value you can see the example for space time
 vector dataset <a href="t.vect.list.html#using-method-option">t.vect.list</a>
 
+<h3>Filtering the result by band references</h3>
+
+Band reference can be assigned to raster maps
+by <em><a href="i.band.html">i.band</a></em> module or even when
+registrating raster maps into STRDS
+by <em><a href="t.register.html#support-for-band-references">t.register</a></em>.
+
+<p>
+Name of STRDS can be extended by band reference identifier used for
+filtering. Name of STRDS and band reference is split by a <i>single
+dot</i>.
+
+<div class="code"><pre>
+t.rast.list input=test.S2_1
+</pre></div>
+
+
+Note that band reference filtering is <i>supported by all temporal
+modules</i>.
+
+<p>
+Also note that only STRDS can be filtered by band reference
+identifier, see <em><a href="i.band.html#known-issues">i.band</a></em> for
+current limitations.
+
+
 <h2>SEE ALSO</h2>
 
 <em>

+ 25 - 11
temporal/t.rast.mapcalc/t.rast.mapcalc.html

@@ -5,32 +5,32 @@
 raster datasets (STRDS). Spatial and temporal operators and internal
 variables are available in the expression string. The description of
 the spatial operators, functions and internal variables is available in
-the <a href="r.mapcalc.html">r.mapcalc</a> manual page. The temporal
+the <em><a href="r.mapcalc.html">r.mapcalc</a></em> manual page. The temporal
 functions are described in detail below.
 <p>
 This module expects several parameters. All space time raster datasets
 that are referenced in the <em>mapcalc expression</em> must be listed
-in the <em>input</em> option. The <em>first</em> space time raster
+in the <b>inputs</b> option. The <em>first</em> space time raster
 dataset that is listed as input will be used to temporally sample all
 other space time raster datasets. The temporal sampling method can be
-chosen using the <em>method</em> option. The order of the STRDS's in
+chosen using the <b>method</b> option. The order of the STRDS's in
 the mapcalc expression can be different to the order of the STRDS's in
 the input option. The resulting space time raster dataset must be
-specified in the <em>output</em> option together with the <em>basename</em>
+specified in the <b>output</b> option together with the <b>basename</b>
 of generated raster maps that are registered in the resulting
 STRDS. Empty maps resulting from map-calculation are not registered by
-default. This behavior can be changed with the <em>-n</em> flag. The
-flag <em>-s</em> can be used to assure that only spatially related maps
+default. This behavior can be changed with the <b>-n</b> flag. The
+flag <b>-s</b> can be used to assure that only spatially related maps
 in the STRDS's are processed. Spatially related means that temporally
 related maps overlap in their spatial extent.
 <p>
 The module <em>t.rast.mapcalc</em> supports parallel processing. The option
-<em>nprocs</em> specifies the number of processes that can be started in
+<b>nprocs</b> specifies the number of processes that can be started in
 parallel.
 <p>
 A mapcalc expression must be provided to process the temporal
 sampled maps. Temporal internal variables are available in addition to
-the <em>r.mapcalc</em> spatial operators and functions:
+the <em><a href="r.mapcalc.html">r.mapcalc</a></em> spatial operators and functions:
 <p>
 The supported internal variables for relative and absolute time are:
 <ul>
@@ -95,7 +95,7 @@ t.rast.mapcalc input=A,B output=C basename=c method=equal \
     expression="if(start_month() == 5 || start_month() == 6, (A + B), (A * B))"
 </pre></div>
 <p>
-The resulting raster maps in dataset C can be listed with <em>t.rast.list</em>:
+The resulting raster maps in dataset C can be listed with <em><a href="t.rast.list.html">t.rast.list</a></em>:
 <p>
 <div class="code"><pre>
 name    start_time              min     max
@@ -111,7 +111,7 @@ Internally the spatio-temporal expression will be analyzed for each
 time interval of the sample dataset A, the temporal functions will be
 replaced by numerical values, the names of the space time raster
 datasets will be replaced by the corresponding raster maps. The final
-expression will be passed to <em>r.mapcalc</em>, resulting in 6 runs:
+expression will be passed to <em><a href="r.mapcalc.html">r.mapcalc</a></em>, resulting in 6 runs:
 <p>
 <div class="code"><pre>
 r.mapcalc expression="c_1 = if(3 == 5 || 3 == 6, (a3 + b3), (a3 * b3))"
@@ -123,7 +123,7 @@ r.mapcalc expression="c_6 = if(8 == 5 || 8 == 6, (a8 + b8), (a8 * b8))"
 </pre></div>
 <p>
 
-<h2>EXAMPLE</h2>
+<h2>EXAMPLES</h2>
 
 The following command creates a new space time raster dataset 
 <tt>january_under_0</tt> that will set to null all cells with
@@ -153,6 +153,20 @@ t.rast.list tempmean_monthly columns=name,start_time,min,max | grep 01-01
 2012_01_tempmean|2012-01-01 00:00:00|-0.534994|9.69511
 </pre></div>
 
+<h3>Band reference filtering</h3>
+
+<em>t.rast.mapcalc</em> supports band reference filtering similarly
+to <em><a href="t.rast.list.html#filtering-the-result-by-band-references">t.rast.list</a></em>. In
+example below a new STRDS will be created and filled by NDVI products.
+
+<div class="code"><pre>
+t.rast.mapcalc inputs=test.S2_8,test.S2_4 output=ndvi basename=ndvi \
+     expression="float(test.S2_8 - test.S2_4) / (test.S2_8 + test.S2_4)"
+</pre></div>
+
+For more information about band reference concept
+see <em><a href="g.bands.html">g.bands</a></em> module.
+
 <h2>SEE ALSO</h2>
 
 <em>

+ 39 - 0
temporal/t.register/t.register.html

@@ -113,6 +113,45 @@ prec_5|2002-01-01|2002-04-01
 prec_6|2002-04-01|2002-07-01
 </pre></div>
 
+<h3>Support for band references</h3>
+
+For more information about band references and image collections
+see <em><a href="g.bands.html">g.bands</a></em> module.
+
+<p>
+Specification of map names and absolute start time (datetime) of the time
+instances. The last column indicates related band references.
+
+<div class="code"><pre>
+T33UYP_20190331T094039_B01|2019-03-31 09:40:39|S2_1
+T33UYP_20190331T094039_B10|2019-03-31 09:40:39|S2_10
+T33UYP_20190331T094039_B02|2019-03-31 09:40:39|S2_2
+T33UYP_20190331T094039_B05|2019-03-31 09:40:39|S2_5
+T33UYP_20190331T094039_B11|2019-03-31 09:40:39|S2_11
+T33UYP_20190331T094039_B08|2019-03-31 09:40:39|S2_8
+T33UYP_20190331T094039_B12|2019-03-31 09:40:39|S2_12
+T33UYP_20190331T094039_B8A|2019-03-31 09:40:39|S2_8A
+T33UYP_20190331T094039_B06|2019-03-31 09:40:39|S2_6
+T33UYP_20190331T094039_B04|2019-03-31 09:40:39|S2_4
+T33UYP_20190331T094039_B03|2019-03-31 09:40:39|S2_3
+T33UYP_20190331T094039_B09|2019-03-31 09:40:39|S2_9
+</pre></div>
+
+In this case <em>t.register</em> assignes to given raster maps band
+references similarly as
+<em><a href="i.band.html">i.band</a></em> does.
+
+Such registered raster maps is possible
+to <a href="t.rast.list.html#filtering-the-result-by-band-references">filter
+by a band references</a>.
+
+<p>
+Please note that raster maps with band references assigned can be
+registered only in STRDS created in TGIS DB version 3 or
+higher. <i>Older versions of TGIS DB are not supported.</i> TGIS DB
+version can be checked <em><a href="t.connect.html">t.connect</a></em>
+module.
+
 <h2>EXAMPLE</h2>
 
 <h3>North Carolina dataset</h3>