Ver código fonte

Integrate band references into portable signature files (#1501)

Imagery: untie signature files from groups

Thus far signature files have been living inside an imagery group. Mapping between individual raster maps of a group and signature values was implicit. This old design made impossible to safely re-use signatures of one imagery group to classify other group(s). The new approach is to have raster band references written inside a signature file to store mapping of signature values to rasters they represent. This change allows to safely use signature file for classification of other imagery groups as long as they contain rasters of the same semantic content (e.g. different image form the same satellite).

* On signature file creation write out raster band references
* On signature file reading compare group and signature file band references for a match
* Signature files now live outside of groups in a dedicated folder with subfolders for each signature file type
* All imagery modules are adjusted to use new signature handling functions
Māris Nartišs 3 anos atrás
pai
commit
5afcea7f95
50 arquivos alterados com 3575 adições e 471 exclusões
  1. 4 0
      .github/workflows/build_centos.sh
  2. 10 34
      gui/wxpython/gui_core/forms.py
  3. 31 30
      gui/wxpython/gui_core/gselect.py
  4. 2 12
      gui/wxpython/iclass/dialogs.py
  5. 24 10
      gui/wxpython/iclass/frame.py
  6. 6 2
      imagery/i.cca/i.cca.html
  7. 17 4
      imagery/i.cca/main.c
  8. 2 1
      imagery/i.cluster/global.h
  9. 32 11
      imagery/i.cluster/i.cluster.html
  10. 14 13
      imagery/i.cluster/main.c
  11. 18 4
      imagery/i.cluster/open_files.c
  12. 1 1
      imagery/i.gensig/files.h
  13. 0 1
      imagery/i.gensig/get_train.c
  14. 35 26
      imagery/i.gensig/i.gensig.html
  15. 2 1
      imagery/i.gensig/main.c
  16. 14 3
      imagery/i.gensig/openfiles.c
  17. 1 1
      imagery/i.gensig/parms.h
  18. 8 10
      imagery/i.gensig/parse.c
  19. 1 2
      imagery/i.gensig/write_sig.c
  20. 0 2
      imagery/i.gensigset/get_train.c
  21. 17 15
      imagery/i.gensigset/i.gensigset.html
  22. 1 1
      imagery/i.gensigset/main.c
  23. 12 2
      imagery/i.gensigset/openfiles.c
  24. 2 2
      imagery/i.gensigset/parms.h
  25. 7 9
      imagery/i.gensigset/parse.c
  26. 3 6
      imagery/i.gensigset/subcluster.c
  27. 1 2
      imagery/i.gensigset/write_sig.c
  28. 9 5
      imagery/i.maxlik/i.maxlik.html
  29. 3 3
      imagery/i.maxlik/main.c
  30. 21 13
      imagery/i.maxlik/open.c
  31. 2 4
      imagery/i.smap/bouman.h
  32. 36 16
      imagery/i.smap/i.smap.html
  33. 1 2
      imagery/i.smap/main.c
  34. 25 1
      imagery/i.smap/openfiles.c
  35. 1 1
      imagery/i.smap/parse.c
  36. 0 35
      imagery/i.smap/read_sig.c
  37. 6 0
      imagery/imageryintro.html
  38. 19 8
      include/grass/defs/imagery.h
  39. 27 13
      include/grass/imagery.h
  40. 58 29
      lib/imagery/find.c
  41. 13 10
      lib/imagery/iclass_signatures.c
  42. 292 0
      lib/imagery/manage_signatures.c
  43. 315 9
      lib/imagery/sig.c
  44. 26 36
      lib/imagery/sigfile.c
  45. 268 20
      lib/imagery/sigset.c
  46. 37 61
      lib/imagery/sigsetfile.c
  47. 151 0
      lib/imagery/testsuite/test_imagery_find.py
  48. 685 0
      lib/imagery/testsuite/test_imagery_sigfile.py
  49. 768 0
      lib/imagery/testsuite/test_imagery_signature_management.py
  50. 547 0
      lib/imagery/testsuite/test_imagery_sigsetfile.py

+ 4 - 0
.github/workflows/build_centos.sh

@@ -21,6 +21,10 @@ set -u
 
 export INSTALL_PREFIX=$1
 
+# Old versions of GCC on CentOS default to C89 although are C11 capable
+# This causes compilation to fail on >C89 code
+export CFLAGS="-O2 -std=gnu11"
+
 ./configure \
     --prefix="$INSTALL_PREFIX/" \
     --without-freetype \

+ 10 - 34
gui/wxpython/gui_core/forms.py

@@ -383,28 +383,6 @@ class UpdateThread(Thread):
             elif name == "SubGroupSelect":
                 self.data[win.Insert] = {"group": p.get("value", "")}
 
-            elif name == "SignatureSelect":
-                if p.get("prompt", "group") == "group":
-                    group = p.get("value", "")
-                    pSubGroup = self.task.get_param(
-                        "subgroup", element="prompt", raiseError=False
-                    )
-                    if pSubGroup:
-                        subgroup = pSubGroup.get("value", "")
-                    else:
-                        subgroup = None
-                else:
-                    subgroup = p.get("value", "")
-                    pGroup = self.task.get_param(
-                        "group", element="prompt", raiseError=False
-                    )
-                    if pGroup:
-                        group = pGroup.get("value", "")
-                    else:
-                        group = None
-
-                self.data[win.Insert] = {"group": group, "subgroup": subgroup}
-
             elif name == "LocationSelect":
                 pDbase = self.task.get_param(
                     "dbase", element="element", raiseError=False
@@ -1756,8 +1734,16 @@ class CmdPanel(wx.Panel):
 
                 # sigrature file
                 elif prompt == "sigfile":
+                    if p.get("age", "") == "new":
+                        mapsets = [
+                            grass.gisenv()["MAPSET"],
+                        ]
+                    else:
+                        mapsets = None
                     selection = gselect.SignatureSelect(
-                        parent=which_panel, element=p.get("element", "sig")
+                        parent=which_panel,
+                        element=p.get("element", "sig"),
+                        mapsets=mapsets,
                     )
                     p["wxId"] = [selection.GetId()]
                     selection.Bind(wx.EVT_TEXT, self.OnSetValue)
@@ -2365,7 +2351,6 @@ class CmdPanel(wx.Panel):
         pColumn = []
         pGroup = None
         pSubGroup = None
-        pSigFile = []
         pDbase = None
         pLocation = None
         pMapset = None
@@ -2412,8 +2397,6 @@ class CmdPanel(wx.Panel):
                 pGroup = p
             elif prompt == "subgroup":
                 pSubGroup = p
-            elif prompt == "sigfile":
-                pSigFile.append(p)
             elif prompt == "dbase":
                 pDbase = p
             elif prompt == "location":
@@ -2430,9 +2413,6 @@ class CmdPanel(wx.Panel):
         pLayerIds = []
         for p in pLayer:
             pLayerIds += p["wxId"]
-        pSigFileIds = []
-        for p in pSigFile:
-            pSigFileIds += p["wxId"]
         pSqlWhereIds = []
         for p in pSqlWhere:
             pSqlWhereIds += p["wxId"]
@@ -2459,11 +2439,7 @@ class CmdPanel(wx.Panel):
             pTable["wxId-bind"] = pColumnIds
 
         if pGroup and pSubGroup:
-            if pSigFile:
-                pGroup["wxId-bind"] = pSigFileIds + pSubGroup["wxId"]
-                pSubGroup["wxId-bind"] = pSigFileIds
-            else:
-                pGroup["wxId-bind"] = pSubGroup["wxId"]
+            pGroup["wxId-bind"] = pSubGroup["wxId"]
 
         if pDbase and pLocation:
             pDbase["wxId-bind"] = pLocation["wxId"]

+ 31 - 30
gui/wxpython/gui_core/gselect.py

@@ -47,6 +47,7 @@ import os
 import sys
 import glob
 import six
+import ctypes
 
 import wx
 
@@ -58,6 +59,13 @@ import wx.lib.filebrowsebutton as filebrowse
 import grass.script as grass
 from grass.script import task as gtask
 from grass.exceptions import CalledModuleError
+from grass.lib.imagery import (
+    I_SIGFILE_TYPE_SIG,
+    I_SIGFILE_TYPE_SIGSET,
+    I_signatures_list_by_type,
+    I_free_signatures_list,
+)
+from grass.pygrass.utils import decode
 
 from gui_core.widgets import ManageSettingsWidget, CoordinatesValidator
 
@@ -2798,44 +2806,37 @@ class SignatureSelect(wx.ComboBox):
         self,
         parent,
         element,
+        mapsets,
         id=wx.ID_ANY,
         size=globalvar.DIALOG_GSELECT_SIZE,
         **kwargs,
     ):
         super(SignatureSelect, self).__init__(parent, id, size=size, **kwargs)
-        self.element = element
-        self.SetName("SignatureSelect")
-
-    def Insert(self, group, subgroup=None):
-        """Insert signatures for defined group/subgroup
 
-        :param group: group name (can be fully-qualified)
-        :param subgroup: non fully-qualified name of subgroup
-        """
-        if not group:
-            return
-        gisenv = grass.gisenv()
-        try:
-            name, mapset = group.split("@", 1)
-        except ValueError:
-            name = group
-            mapset = gisenv["MAPSET"]
-
-        path = os.path.join(
-            gisenv["GISDBASE"], gisenv["LOCATION_NAME"], mapset, "group", name
-        )
-
-        if subgroup:
-            path = os.path.join(path, "subgroup", subgroup)
-        try:
-            items = list()
-            for element in os.listdir(os.path.join(path, self.element)):
-                items.append(element)
-            self.SetItems(items)
-        except OSError:
-            self.SetItems([])
+        sig_type = None
+        # Extend here if a new signature type is introduced
+        if element == "signatures/sig":
+            sig_type = I_SIGFILE_TYPE_SIG
+        elif element == "signatures/sigset":
+            sig_type = I_SIGFILE_TYPE_SIGSET
+        items = []
+        if sig_type is not None:
+            if mapsets:
+                for mapset in mapsets:
+                    self._append_mapset_signatures(mapset, sig_type, items)
+            else:
+                self._append_mapset_signatures(None, sig_type, items)
+        self.SetItems(items)
         self.SetValue("")
 
+    def _append_mapset_signatures(self, mapset, sig_type, items):
+        list_ptr = ctypes.POINTER(ctypes.c_char_p)
+        sig_list = list_ptr()
+        count = I_signatures_list_by_type(sig_type, mapset, ctypes.byref(sig_list))
+        for n in range(count):
+            items.append(decode(sig_list[n]))
+        I_free_signatures_list(count, sig_list)
+
 
 class SeparatorSelect(wx.ComboBox):
     """Widget for selecting seperator"""

+ 2 - 12
gui/wxpython/iclass/dialogs.py

@@ -599,8 +599,6 @@ class IClassSignatureFileDialog(wx.Dialog):
     def __init__(
         self,
         parent,
-        group,
-        subgroup,
         file=None,
         title=_("Save signature file"),
         id=wx.ID_ANY,
@@ -610,7 +608,6 @@ class IClassSignatureFileDialog(wx.Dialog):
         """Dialog for saving signature file
 
         :param parent: window
-        :param group: group name
         :param file: signature file name
         :param title: window title
         """
@@ -622,16 +619,9 @@ class IClassSignatureFileDialog(wx.Dialog):
 
         # inconsistent group and subgroup name
         # path:
-        # grassdata/nc_spm_08/landsat/group/test_group/subgroup/test_group/sig/sigFile
+        # grassdata/nc_spm_08/landsat/signatures/sig/sigFile
         self.baseFilePath = os.path.join(
-            env["GISDBASE"],
-            env["LOCATION_NAME"],
-            env["MAPSET"],
-            "group",
-            group,
-            "subgroup",
-            subgroup,
-            "sig",
+            env["GISDBASE"], env["LOCATION_NAME"], env["MAPSET"], "signatures", "sig"
         )
         self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
 

+ 24 - 10
gui/wxpython/iclass/frame.py

@@ -1117,7 +1117,17 @@ class IClassMapFrame(DoubleMapFrame):
             return False
 
         I_free_signatures(self.signatures)
-        I_iclass_init_signatures(self.signatures, self.refer)
+        ret = I_iclass_init_signatures(self.signatures, self.refer)
+        if not ret:
+            GMessage(
+                parent=self,
+                message=_(
+                    "There was an error initializing signatures. "
+                    "Check GUI console for any error messages."
+                ),
+            )
+            I_free_signatures(self.signatures)
+            return False
 
         # why create copy
         # cats = self.statisticsList[:]
@@ -1201,9 +1211,16 @@ class IClassMapFrame(DoubleMapFrame):
                 qdlg.Destroy()
                 return
 
-        dlg = IClassSignatureFileDialog(
-            self, group=self.g["group"], subgroup=self.g["subgroup"], file=self.sigFile
-        )
+        if not self.signatures.contents.nsigs:
+            GMessage(
+                parent=self,
+                message=_(
+                    "Signatures are not valid. Recalculate them and then try again."
+                ),
+            )
+            return
+
+        dlg = IClassSignatureFileDialog(self, file=self.sigFile)
 
         if dlg.ShowModal() == wx.ID_OK:
             if os.path.exists(dlg.GetFileName(fullPath=True)):
@@ -1223,9 +1240,7 @@ class IClassMapFrame(DoubleMapFrame):
                     qdlg.Destroy()
                     return
             self.sigFile = dlg.GetFileName()
-            self.WriteSignatures(
-                self.signatures, self.g["group"], self.g["subgroup"], self.sigFile
-            )
+            self.WriteSignatures(self.signatures, self.sigFile)
 
         dlg.Destroy()
 
@@ -1248,14 +1263,13 @@ class IClassMapFrame(DoubleMapFrame):
         self.refer = pointer(refer_obj)
         I_init_group_ref(self.refer)  # must be freed on exit
 
-    def WriteSignatures(self, signatures, group, subgroup, filename):
+    def WriteSignatures(self, signatures, filename):
         """Writes current signatures to signature file
 
         :param signatures: signature (c structure)
-        :param group: imagery group
         :param filename: signature file name
         """
-        I_iclass_write_signatures(signatures, group, subgroup, filename)
+        I_iclass_write_signatures(signatures, filename)
 
     def CheckInput(self, group, vector):
         """Check if input is valid"""

+ 6 - 2
imagery/i.cca/i.cca.html

@@ -60,6 +60,8 @@ and the current mask setting while performing the transformation.
 
 <em>
 <a href="g.gui.iclass.html">g.gui.iclass</a>,
+<a href="i.gensig.html">i.gensig</a>,
+<a href="i.cluster.html">i.cluster</a>,
 <a href="i.pca.html">i.pca</a>,
 <a href="r.covar.html">r.covar</a>,
 <a href="r.mapcalc.html">r.mapcalc</a>
@@ -74,11 +76,13 @@ Classification in Remote Sensing</b>,  Academic Press, 1983.
 
 David Satnik, GIS Laboratory, 
 Central Washington University
-
 <br>
-
 Ali R. Vali, 
 University of Texas
+<br>
+Band reference support: Maris Nartiss,
+University of Latvia
+
 <!--
 <p>
 <i>Last changed: $Date$</i>

+ 17 - 4
imagery/i.cca/main.c

@@ -71,6 +71,7 @@ int main(int argc, char *argv[])
     struct Signature sigs;
     FILE *sigfp;
     struct Ref refs;
+    char **err;
     int *datafds;
     int *outfds;
 
@@ -91,13 +92,14 @@ int main(int argc, char *argv[])
 
     grp_opt = G_define_standard_option(G_OPT_I_GROUP);
 
-    subgrp_opt = G_define_standard_option(G_OPT_I_GROUP);
+    subgrp_opt = G_define_standard_option(G_OPT_I_SUBGROUP);
     subgrp_opt->key = "subgroup";
     subgrp_opt->description = _("Name of input imagery subgroup");
 
     sig_opt = G_define_option();
     sig_opt->key = "signature";
     sig_opt->type = TYPE_STRING;
+    sig_opt->gisprompt = "old,signatures/sig,sigfile";
     sig_opt->required = YES;
     sig_opt->key_desc = "name";
     sig_opt->description = _("File containing spectral signatures");
@@ -113,16 +115,18 @@ int main(int argc, char *argv[])
     if (I_find_group(grp_opt->answer) <= 0)
 	G_fatal_error(_("Unknown imagery group."));
 
+    if (!I_find_subgroup(grp_opt->answer, subgrp_opt->answer))
+	G_fatal_error(_("Subgroup <%s> in group <%s> not found"),
+		      subgrp_opt->answer, grp_opt->answer);
+
     if (I_get_subgroup_ref(grp_opt->answer, subgrp_opt->answer, &refs) <= 0)
 	G_fatal_error(_("Unable to find subgroup reference information."));
 
     /* open and input the signatures file */
     if ((sigfp =
-	 I_fopen_signature_file_old(grp_opt->answer, subgrp_opt->answer,
-				    sig_opt->answer)) == NULL)
+	 I_fopen_signature_file_old(sig_opt->answer)) == NULL)
 	G_fatal_error(_("Unable to open the signature file"));
 
-    I_init_signatures(&sigs, refs.nfiles);
     if (I_read_signatures(sigfp, &sigs) < 0)
 	G_fatal_error(_("Error while reading the signatures file."));
 
@@ -131,6 +135,15 @@ int main(int argc, char *argv[])
     if (nclass < 2)
 	G_fatal_error(_("Need at least two signatures in signature file."));
 
+    err = I_sort_signatures_by_bandref(&sigs, &refs);
+    if (err)
+        G_fatal_error(_("Signature – group member band reference mismatch.\n"
+            "Extra signatures for bands: %s\n"
+            "Imagery group bands without signatures: %s"),
+            err[0] ? err[0] : _("none"),
+            err[1] ? err[1] : _("none")
+        );
+
     /* check the number of input bands */
     bands = refs.nfiles;
 

+ 2 - 1
imagery/i.cluster/global.h

@@ -26,7 +26,8 @@ extern int mcs;
 extern char *group;
 extern char *subgroup;
 extern struct Ref ref;
-extern char *outsigfile;
+extern char **bandrefs;
+extern char outsigfile[GNAME_MAX + GMAPSET_MAX];
 extern char *insigfile;
 extern char *reportfile;
 extern DCELL **cell;

+ 32 - 11
imagery/i.cluster/i.cluster.html

@@ -71,6 +71,14 @@ The classes value is the initial number of clusters to be
 discriminated; any parameter values left unspecified are
 set to their default values.
 
+<p>
+All raster maps used to generate signature file must have band reference
+set. Use <em><a href="r.support.html">r.support</a></em> to set
+band references of each member of the imagery group.
+Signatures generated for one scene are suitable for classification
+of other scenes as long as they consist of same raster bands
+(band references match).
+
 <h3>Parameters:</h3>
 
 <dl>
@@ -208,13 +216,6 @@ achieve the convergence, and the separability matrix.
 
 <h2>NOTES</h2>
 
-<!-- No longer true
-Running in command line mode, <em>i.cluster</em> will
-overwrite the output signature file and reportfile (if
-required by the user) without prompting if the files
-existed.
--->
-
 <h3>Sampling method</h3>
 
 <em>i.cluster</em> does not cluster all pixels, but only a sample (see
@@ -262,15 +263,32 @@ Preparing the statistics for unsupervised classification of
 a LANDSAT subscene in North Carolina:
 
 <div class="code"><pre>
+## Prepare imagery for classification
+# Skip this step if your rasters already have band references defined
+# Define band references for all LANDSAT bands in mapset PERMANENT
+g.mapset mapset=PERMANENT
+r.support map=lsat7_2002_10 bandref=TM7_1
+r.support map=lsat7_2002_20 bandref=TM7_2
+r.support map=lsat7_2002_30 bandref=TM7_3
+r.support map=lsat7_2002_40 bandref=TM7_4
+r.support map=lsat7_2002_50 bandref=TM7_5
+r.support map=lsat7_2002_61 bandref=TM7_61
+r.support map=lsat7_2002_62 bandref=TM7_62
+r.support map=lsat7_2002_70 bandref=TM7_7
+r.support map=lsat7_2002_80 bandref=TM7_8
+g.mapset mapset=user1  # replace user1 with your mapset name
+
+## Real work starts here
+# Set computational region to match the scene
 g.region raster=lsat7_2002_10 -p
 
 # store VIZ, NIR, MIR into group/subgroup (leaving out TIR)
-i.group group=lsat7_2002 subgroup=lsat7_2002 \
+i.group group=lsat7_2002 subgroup=res_30m \
   input=lsat7_2002_10,lsat7_2002_20,lsat7_2002_30,lsat7_2002_40,lsat7_2002_50,lsat7_2002_70
 
 # generate signature file and report
-i.cluster group=lsat7_2002 subgroup=lsat7_2002 \
-  signaturefile=sig_cluster_lsat2002 \
+i.cluster group=lsat7_2002 subgroup=res_30m \
+  signaturefile=cluster_lsat2002 \
   classes=10 reportfile=rep_clust_lsat2002.txt
 </pre></div>
 
@@ -289,6 +307,7 @@ See example in its manual page.
 
 <p>
 <em>
+<a href="r.support.html">r.support</a>,
 <a href="g.gui.iclass.html">g.gui.iclass</a>,
 <a href="i.group.html">i.group</a>,
 <a href="i.gensig.html">i.gensig</a>,
@@ -302,10 +321,12 @@ See example in its manual page.
 
 Michael Shapiro,
 U.S. Army Construction Engineering Research Laboratory
-
 <br>
 Tao Wen, 
 University of Illinois at Urbana-Champaign, Illinois
+<br>
+Band reference support: Maris Nartiss,
+University of Latvia
 
 <!--
 <p>

+ 14 - 13
imagery/i.cluster/main.c

@@ -39,7 +39,8 @@ int mcs;
 char *group;
 char *subgroup;
 struct Ref ref;
-char *outsigfile;
+char **bandrefs;
+char outsigfile[GNAME_MAX + GMAPSET_MAX];
 char *insigfile;
 char *reportfile;
 DCELL **cell;
@@ -60,6 +61,7 @@ int main(int argc, char *argv[])
     DCELL *x;
     struct Cell_head window;
     FILE *fd;
+    char xmapset[GMAPSET_MAX];
 
     struct GModule *module;
     struct
@@ -91,7 +93,7 @@ int main(int argc, char *argv[])
     parm.out_sig->type = TYPE_STRING;
     parm.out_sig->key_desc = "name";
     parm.out_sig->required = YES;
-    parm.out_sig->gisprompt = "new,sig,sigfile";
+    parm.out_sig->gisprompt = "new,signatures/sig,sigfile";
     parm.out_sig->description = _("Name for output file containing result signatures");
 
     parm.class = G_define_option();
@@ -107,7 +109,7 @@ int main(int argc, char *argv[])
     parm.seed_sig->required = NO;
     parm.seed_sig->type = TYPE_STRING;
     parm.seed_sig->key_desc = "name";
-    parm.seed_sig->gisprompt = "old,sig,sigfile";
+    parm.seed_sig->gisprompt = "old,signatures/sig,sigfile";
     parm.seed_sig->description = _("Name of file containing initial signatures");
 
     parm.sample_interval = G_define_option();
@@ -162,7 +164,6 @@ int main(int argc, char *argv[])
 
     group = parm.group_name->answer;	/* a required parameter */
     subgroup = parm.subgroup_name->answer;	/* required */
-    outsigfile = parm.out_sig->answer;
     
     /* check all the inputs */
     if (!I_find_group(group)) {
@@ -171,14 +172,12 @@ int main(int argc, char *argv[])
     if (!I_find_subgroup(group, subgroup)) {
         G_fatal_error(_("Subgroup <%s> in group <%s> not found"), subgroup, group);
     }
-    
-    /* GRASS parser fails to detect existing signature files as
-     * detection needs answers from other parameters as group and subgroup.
-     * Thus check is performed only now. */
-    if (!G_get_overwrite() && I_find_signature_file(group, subgroup, "sig", outsigfile)) {
-        G_fatal_error(_("option <%s>: <%s> exists. To overwrite, use the --overwrite flag"),
-                        parm.out_sig->key, parm.out_sig->answer);
-    }
+
+    if (G_unqualified_name(parm.out_sig->answer, G_mapset(), outsigfile, xmapset) < 0)
+        G_fatal_error(_("<%s> does not match the current mapset"), xmapset);
+
+    if (G_legal_filename(outsigfile) < 0)
+        G_fatal_error(_("<%s> is an illegal file name"), outsigfile);
 
     G_get_window(&window);
     nrows = Rast_window_rows();
@@ -333,7 +332,9 @@ int main(int argc, char *argv[])
     print_class_means(report, &C);
 
     if ((fd =
-	 I_fopen_signature_file_new(group, subgroup, outsigfile)) != NULL) {
+	 I_fopen_signature_file_new(outsigfile)) != NULL) {
+        for (unsigned int i = C.S.nbands; i--;)
+            C.S.bandrefs[i] = bandrefs[i];
 	I_write_signatures(fd, &C.S);
 	fclose(fd);
     }

+ 18 - 4
imagery/i.cluster/open_files.c

@@ -7,7 +7,7 @@
 
 int open_files(void)
 {
-    char *name, *mapset;
+    char *name, *mapset, **err, *bandref;
     FILE *fd;
     int n, missing;
 
@@ -15,6 +15,7 @@ int open_files(void)
     I_free_group_ref(&ref);
     I_get_subgroup_ref(group, subgroup, &ref);
 
+    bandrefs = (char **)G_malloc(ref.nfiles * sizeof(char **));
     missing = 0;
     for (n = 0; n < ref.nfiles; n++) {
 	name = ref.file[n].name;
@@ -23,7 +24,12 @@ int open_files(void)
 	    missing = 1;
 	    G_warning(_("Raster map <%s> do not exists in subgroup <%s>"),
 		      G_fully_qualified_name(name, mapset), subgroup);
-	}
+        }
+        bandref = Rast_read_bandref(ref.file[n].name, ref.file[n].mapset);
+        if (!bandref)
+            G_fatal_error(_("Raster map <%s@%s> lacks band reference"),
+                            ref.file[n].name, ref.file[n].mapset);
+        bandrefs[n] = G_store(bandref);
     }
     if (missing)
 	G_fatal_error(_("No raster maps found"));
@@ -47,9 +53,8 @@ int open_files(void)
 	cellfd[n] = Rast_open_old(name, mapset);
     }
 
-    I_init_signatures(&in_sig, ref.nfiles);
     if (insigfile) {
-	fd = I_fopen_signature_file_old(group, subgroup, insigfile);
+	fd = I_fopen_signature_file_old(insigfile);
 	if (fd == NULL)
 	    G_fatal_error(_("Unable to open seed signature file <%s>"),
 			  insigfile);
@@ -64,6 +69,15 @@ int open_files(void)
 	    G_fatal_error(_("<%s> has too many signatures (limit is 255)"),
 			  insigfile);
 
+        err = I_sort_signatures_by_bandref(&in_sig, &ref);
+        if (err)
+            G_fatal_error(_("Signature – group member band reference mismatch.\n"
+                "Extra signatures for bands: %s\n"
+                "Imagery group bands without signatures: %s"),
+                err[0] ? err[0] : _("none"),
+                err[1] ? err[1] : _("none")
+            );
+
 	maxclass = in_sig.nsigs;
     }
 

+ 1 - 1
imagery/i.gensig/files.h

@@ -18,5 +18,5 @@ int read_training_map(CELL *, int, int, struct files *);
 
 #ifdef _l_parms_h
 void read_training_labels(struct parms *, struct files *);
-int openfiles(struct parms *, struct files *);
+int openfiles(struct parms *, struct files *, struct Signature *);
 #endif

+ 0 - 1
imagery/i.gensig/get_train.c

@@ -27,7 +27,6 @@ int get_training_classes(struct files *files, struct Signature *S)
     ncols = Rast_window_cols();
 
     /* determine the categories in the map */
-    I_init_signatures(S, files->nbands);
     Rast_init_cell_stats(&cell_stats);
     G_message(_("Finding training classes..."));
     for (row = 0; row < nrows; row++) {

+ 35 - 26
imagery/i.gensig/i.gensig.html

@@ -21,6 +21,14 @@ The user would then execute the GRASS program
 <em><a href="i.maxlik.html">i.maxlik</a></em>
 to actually create the final classified map.
 
+<p>
+All raster maps used to generate signature file must have band reference
+set. Use <em><a href="r.support.html">r.support</a></em> to set
+band references of each member of the imagery group.
+Signatures generated for one scene are suitable for classification
+of other scenes as long as they consist of same raster bands
+(band references match).
+
 <h2>OPTIONS</h2>
 
 <h3>Parameters</h3>
@@ -66,21 +74,11 @@ that form an image.
 This is the resultant signature file (containing the means
 and covariance matrices) for each class in the training map
 that is associated with the band files in the subgroup
-select (see <a href="#subgroup">above</a>).
+select (see <a href="#subgroup">above</a>). Resultant singature file
+can be used with any other imagery group as long as band references
+match.
 </dl>
 
-<h2>INTERACTIVE MODE</h2>
-
-If none of the arguments are specified on the command line, 
-<em>i.gensig</em>
-will interactively prompt for the names of these maps and files.
-
-<p>
-It should be noted that interactive mode here only means
-interactive prompting for maps and files.
-It does not mean visualization of the signatures that
-result from the process.
-
 <h2>NOTES</h2>
 
 The structure of the SIG files generated by <em>i.gensig</em> is
@@ -91,30 +89,38 @@ added here for explanation only</i>:
 <p>
 SIG file "lsat7_2000_gensig":
 <div class="code"><pre>
- 1 #
- 2 #water
- 3 4186
- 4 67.9508 48.7346 37.8915 15.3129 13.8473 12.0855 
- 5 1.74334 
- 6 0.439504 2.07267 
- 7 0.662523 1.63501 4.21189 
- 8 0.530339 2.40757 5.52857 22.433 
- 9 0.561184 2.30762 5.18846 20.5364 20.4926 
-10 0.393218 1.2184 2.63628 9.61528 9.36025 5.85314 
+ 1 1
+ 2 #
+ 3 Band_reference1
+ 4 #water
+ 5 4186
+ 6 67.9508 48.7346 37.8915 15.3129 13.8473 12.0855 
+ 7 1.74334 
+ 8 0.439504 2.07267 
+ 9 0.662523 1.63501 4.21189 
+10 0.530339 2.40757 5.52857 22.433 
+11 0.561184 2.30762 5.18846 20.5364 20.4926 
+12 0.393218 1.2184 2.63628 9.61528 9.36025 5.85314 
 </pre></div>
 
 <ul>
+<li> Line 1: version number (currently always 1)
 <li> Line 2: text label
-<li> Line 3: number of points in class
-<li> Line 4: mean values per band of the class
-<li> Line 5-10: (semi)-matrix of band-band covariance
+<li> Line 3: Space separated list of band references
+<li> Line 4: text label of class
+<li> Line 5: number of points in class
+<li> Line 6: mean values per band of the class
+<li> Line 7-12: (semi)-matrix of band-band covariance
 </ul>
 
 <h2>SEE ALSO</h2>
 
 <em>
+<a href="r.support.html">r.support</a>,
 <a href="g.gui.iclass.html">g.gui.iclass</a>,
 <a href="i.group.html">i.group</a>,
+<a href="i.cca.html">i.cca</a>,
+<a href="i.maxlik.html">i.maxlik</a>,
 <a href="i.smap.html">i.smap</a>,
 <a href="r.info.html">r.info</a>,
 <a href="r.univar.html">r.univar</a>,
@@ -125,6 +131,9 @@ SIG file "lsat7_2000_gensig":
 
 Michael Shapiro,
 U.S.Army Construction Engineering Research Laboratory
+<br>
+Band reference support: Maris Nartiss,
+University of Latvia
 
 <!--
 <p>

+ 2 - 1
imagery/i.gensig/main.c

@@ -46,7 +46,7 @@ int main(int argc, char *argv[])
 	_("Generates statistics for i.maxlik from raster map.");
 
     parse(argc, argv, &parms);
-    openfiles(&parms, &files);
+    openfiles(&parms, &files, &S);
     read_training_labels(&parms, &files);
 
     get_training_classes(&files, &S);
@@ -54,6 +54,7 @@ int main(int argc, char *argv[])
     compute_covariances(&files, &S);
     check_signatures(&S);
     write_sigfile(&parms, &S);
+    I_free_signatures(&S);
 
     G_done_msg(" ");
     

+ 14 - 3
imagery/i.gensig/openfiles.c

@@ -8,10 +8,10 @@
 #include "files.h"
 
 
-int openfiles(struct parms *parms, struct files *files)
+int openfiles(struct parms *parms, struct files *files, struct Signature *S)
 {
     struct Ref Ref;		/* subgroup reference list */
-    const char *mapset;
+    const char *mapset, *bandref;
     int n;
 
 
@@ -33,12 +33,23 @@ int openfiles(struct parms *parms, struct files *files)
     files->train_fd = Rast_open_old(parms->training_map, mapset);
     files->train_cell = Rast_allocate_c_buf();
 
-    /* open all maps for reading */
+    /* prepare signature struct */
+    I_init_signatures(S, Ref.nfiles);
+
+    /* open all maps for reading and
+       store band references of imagery group bands */
     for (n = 0; n < Ref.nfiles; n++) {
 	files->band_fd[n] =
 	    Rast_open_old(Ref.file[n].name, Ref.file[n].mapset);
 	files->band_cell[n] = Rast_allocate_d_buf();
+        bandref = Rast_read_bandref(Ref.file[n].name, Ref.file[n].mapset);
+        if (!bandref)
+            G_fatal_error(_("Raster map <%s@%s> lacks band reference"),
+                            Ref.file[n].name, Ref.file[n].mapset);
+        S->bandrefs[n] = G_store(bandref);
     }
 
+    I_free_group_ref(&Ref);
+
     return 0;
 }

+ 1 - 1
imagery/i.gensig/parms.h

@@ -5,7 +5,7 @@ struct parms
     char *training_map;
     char *group;
     char *subgroup;
-    char *sigfile;
+    char sigfile[GNAME_MAX + GMAPSET_MAX];
 };
 int parse(int, char *[], struct parms *);
 int write_sigfile(struct parms *, struct Signature *);

+ 8 - 10
imagery/i.gensig/parse.c

@@ -8,6 +8,7 @@
 int parse(int argc, char *argv[], struct parms *parms)
 {
     struct Option *group, *subgroup, *sigfile, *trainingmap;
+    char xmapset[GMAPSET_MAX];
 
     trainingmap = G_define_standard_option(G_OPT_R_MAP);
     trainingmap->key = "trainingmap";
@@ -22,7 +23,7 @@ int parse(int argc, char *argv[], struct parms *parms)
     sigfile->type = TYPE_STRING;
     sigfile->key_desc = "name";
     sigfile->required = YES;
-    sigfile->gisprompt = "new,sig,sigfile";
+    sigfile->gisprompt = "new,signatures/sig,sigfile";
     sigfile->description = _("Name for output file containing result signatures");
 
     if (G_parser(argc, argv))
@@ -31,7 +32,6 @@ int parse(int argc, char *argv[], struct parms *parms)
     parms->training_map = trainingmap->answer;
     parms->group = group->answer;
     parms->subgroup = subgroup->answer;
-    parms->sigfile = sigfile->answer;
 
     /* check all the inputs */
     if (G_find_raster(parms->training_map, "") == NULL)
@@ -42,14 +42,12 @@ int parse(int argc, char *argv[], struct parms *parms)
 
     if (!I_find_subgroup(parms->group, parms->subgroup))
 	G_fatal_error(_("Subgroup <%s> in group <%s> not found"), parms->subgroup, parms->group);
-    
-    /* GRASS parser fails to detect existing signature files as
-     * detection needs answers from other parameters as group and subgroup.
-     * Thus check is performed only now. */
-    if (!G_get_overwrite() && I_find_signature_file(parms->group, parms->subgroup, "sig", parms->sigfile)) {
-        G_fatal_error(_("option <%s>: <%s> exists. To overwrite, use the --overwrite flag"), 
-                        sigfile->key, sigfile->answer);
-    }
+
+    if (G_unqualified_name(sigfile->answer, G_mapset(), parms->sigfile, xmapset) < 0)
+        G_fatal_error(_("<%s> does not match the current mapset"), xmapset);
+
+    if (G_legal_filename(parms->sigfile) < 0)
+        G_fatal_error(_("<%s> is an illegal file name"), parms->sigfile);
     
     return 0;
 }

+ 1 - 2
imagery/i.gensig/write_sig.c

@@ -9,8 +9,7 @@ int write_sigfile(struct parms *parms, struct Signature *S)
 {
     FILE *fd;
 
-    fd = I_fopen_signature_file_new(parms->group, parms->subgroup,
-				    parms->sigfile);
+    fd = I_fopen_signature_file_new(parms->sigfile);
     if (fd == NULL)
 	G_fatal_error(_("Unable to create signature file <%s>"),
 		      parms->sigfile);

+ 0 - 2
imagery/i.gensigset/get_train.c

@@ -27,8 +27,6 @@ int get_training_classes(struct parms *parms,
     ncols = Rast_window_cols();
 
     /* determine the non-zero categories in the map */
-    I_InitSigSet(S);
-    I_SigSetNBands(S, files->nbands);
     I_SetSigTitle(S, Rast_get_cats_title(&files->training_labels));
 
     Rast_init_cell_stats(&cell_stats);

+ 17 - 15
imagery/i.gensigset/i.gensigset.html

@@ -18,6 +18,18 @@ The user would then execute the GRASS program <em>
 <a href="i.smap.html">i.smap</a></em> to create the
 final classified map.
 
+<p>
+All raster maps used to generate signature file must have band reference
+set. Use <em><a href="r.support.html">r.support</a></em> to set
+band references of each member of the imagery group.
+Signatures generated for one scene are suitable for classification
+of other scenes as long as they consist of same raster bands
+(band references match).
+
+<p>
+An usage example can be found in <a href="i.smap.html">i.smap</a>
+documentation.
+
 <h2>OPTIONS</h2>
 
 <h3>Parameters</h3>
@@ -110,21 +122,7 @@ starting value with this option.
 </dl>
 
 
-<h2>INTERACTIVE MODE</h2>
-
-If none of the arguments are specified on the command line,
-<em>i.gensigset</em> will interactively prompt for the
-names of these maps and files.
-
-<p>
-It should be noted that interactive mode here only means
-interactive prompting for maps and files.  It does not mean
-visualization of the signatures that result from the
-process.
-
-<p>
-
-<A NAME="notes"></a><h2>NOTES</h2>
+<h2 id="notes">NOTES</h2>
 
 The algorithm in <em>i.gensigset</em> determines the
 parameters of a spectral class model known as a Gaussian
@@ -216,6 +214,7 @@ then the user should check for:
 <h2>SEE ALSO</h2>
 
 <em>
+<a href="r.support">r.support</a>,
 <a href="i.group.html">i.group</a>,
 <a href="i.smap.html">i.smap</a>,
 <a href="r.info.html">r.info</a>,
@@ -230,6 +229,9 @@ School of Electrical Engineering, Purdue University
 <br>
 Michael Shapiro,
 U.S.Army Construction Engineering Research Laboratory
+<br>
+Band reference support: Maris Nartiss,
+University of Latvia
 
 <!--
 <p>

+ 1 - 1
imagery/i.gensigset/main.c

@@ -46,7 +46,7 @@ int main(int argc, char *argv[])
 	_("Generates statistics for i.smap from raster map.");
 
     parse(argc, argv, &parms);
-    openfiles(&parms, &files);
+    openfiles(&parms, &files, &S);
     read_training_labels(&parms, &files);
 
     get_training_classes(&parms, &files, &S);

+ 12 - 2
imagery/i.gensigset/openfiles.c

@@ -7,10 +7,10 @@
 #include "files.h"
 #include "parms.h"
 
-int openfiles(struct parms *parms, struct files *files)
+int openfiles(struct parms *parms, struct files *files, struct SigSet *S)
 {
     struct Ref Ref;		/* subgroup reference list */
-    const char *mapset;
+    const char *mapset, *bandref;
     int n;
 
 
@@ -28,6 +28,9 @@ int openfiles(struct parms *parms, struct files *files)
     files->band_fd = (int *)G_calloc(Ref.nfiles, sizeof(int));
     files->band_cell = (DCELL **) G_calloc(Ref.nfiles, sizeof(DCELL *));
 
+    /* Prepare SigSet structure */
+    I_InitSigSet(S, files->nbands);
+
     /* open training map for reading */
     mapset = G_find_raster2(parms->training_map, "");
     files->train_fd = Rast_open_old(parms->training_map, mapset);
@@ -38,7 +41,14 @@ int openfiles(struct parms *parms, struct files *files)
 	files->band_fd[n] =
 	    Rast_open_old(Ref.file[n].name, Ref.file[n].mapset);
 	files->band_cell[n] = Rast_allocate_d_buf();
+        bandref = Rast_read_bandref(Ref.file[n].name, Ref.file[n].mapset);
+        if (!bandref)
+            G_fatal_error(_("Raster map <%s@%s> lacks band reference"),
+                            Ref.file[n].name, Ref.file[n].mapset);
+        S->bandrefs[n] = G_store(bandref);
     }
 
+    I_free_group_ref(&Ref);
+
     return 0;
 }

+ 2 - 2
imagery/i.gensigset/parms.h

@@ -3,7 +3,7 @@ struct parms
     char *training_map;
     char *group;
     char *subgroup;
-    char *sigfile;
+    char sigfile[GNAME_MAX + GMAPSET_MAX];
     int maxsubclasses;
 };
 
@@ -21,5 +21,5 @@ int get_training_classes(struct parms *, struct files *, struct SigSet *);
 void read_training_labels(struct parms *, struct files *);
 
 /* openfiles.c */
-int openfiles(struct parms *, struct files *);
+int openfiles(struct parms *, struct files *, struct SigSet *);
 #endif

+ 7 - 9
imagery/i.gensigset/parse.c

@@ -7,6 +7,7 @@
 int parse(int argc, char *argv[], struct parms *parms)
 {
     struct Option *group, *subgroup, *sigfile, *trainingmap, *maxsig;
+    char xmapset[GMAPSET_MAX];
 
     trainingmap = G_define_standard_option(G_OPT_R_MAP);
     trainingmap->key = "trainingmap";
@@ -21,7 +22,7 @@ int parse(int argc, char *argv[], struct parms *parms)
     sigfile->type = TYPE_STRING;
     sigfile->key_desc = "name";
     sigfile->required = YES;
-    sigfile->gisprompt = "new,sig,sigfile";
+    sigfile->gisprompt = "new,signatures/sigset,sigfile";
     sigfile->description = _("Name for output file containing result signatures");
 
     maxsig = G_define_option();
@@ -37,7 +38,6 @@ int parse(int argc, char *argv[], struct parms *parms)
     parms->training_map = trainingmap->answer;
     parms->group = group->answer;
     parms->subgroup = subgroup->answer;
-    parms->sigfile = sigfile->answer;
 
     /* check all the inputs */
     if (G_find_raster(parms->training_map, "") == NULL) {
@@ -50,13 +50,11 @@ int parse(int argc, char *argv[], struct parms *parms)
 	G_fatal_error(_("Subgroup <%s> in group <%s> not found"), parms->subgroup, parms->group);
     }
     
-    /* GRASS parser fails to detect existing signature files as
-     * detection needs answers from other parameters as group and subgroup.
-     * Thus check is performed only now. */
-    if (!G_get_overwrite() && I_find_signature_file(parms->group, parms->subgroup, "sigset", parms->sigfile)) {
-        G_fatal_error(_("option <%s>: <%s> exists. To overwrite, use the --overwrite flag"),
-                        sigfile->key, sigfile->answer);
-    }
+    if (G_unqualified_name(sigfile->answer, G_mapset(), parms->sigfile, xmapset) < 0)
+        G_fatal_error(_("<%s> does not match the current mapset"), xmapset);
+
+    if (G_legal_filename(parms->sigfile) < 0)
+        G_fatal_error(_("<%s> is an illegal file name"), parms->sigfile);
     
     if (sscanf(maxsig->answer, "%d", &parms->maxsubclasses) != 1 ||
 	parms->maxsubclasses <= 0) {

+ 3 - 6
imagery/i.gensigset/subcluster.c

@@ -49,8 +49,7 @@ subcluster(struct SigSet *S, int Class_Index, int *Max_num, int maxsubclasses)
     if (first) {
 	int i;
 
-	I_InitSigSet(&min_S);
-	I_SigSetNBands(&min_S, nbands);
+	I_InitSigSet(&min_S, nbands);
 	min_Sig = I_NewClassSig(&min_S);
 
 	/* allocate enough subsignatures in scratch space */
@@ -406,8 +405,7 @@ reduce_order(struct ClassSig *Sig, int nbands, int *min_ii, int *min_jj)
 
     /* allocate scratch space first time subroutine is called */
     if (first) {
-	I_InitSigSet(&S);
-	I_SigSetNBands(&S, nbands);
+	I_InitSigSet(&S, nbands);
 	Sig3 = I_NewClassSig(&S);
 	I_NewSubSig(&S, Sig3);
 	SubSig3 = Sig3->SubSig;
@@ -486,8 +484,7 @@ distance(struct SubSig *SubSig1, struct SubSig *SubSig2, int nbands)
 
     /* allocate scratch space first time subroutine is called */
     if (first) {
-	I_InitSigSet(&S);
-	I_SigSetNBands(&S, nbands);
+	I_InitSigSet(&S, nbands);
 	Sig3 = I_NewClassSig(&S);
 	I_NewSubSig(&S, Sig3);
 	SubSig3 = Sig3->SubSig;

+ 1 - 2
imagery/i.gensigset/write_sig.c

@@ -7,8 +7,7 @@ int write_sigfile(struct parms *parms, struct SigSet *S)
 {
     FILE *fd;
 
-    fd = I_fopen_sigset_file_new(parms->group, parms->subgroup,
-				 parms->sigfile);
+    fd = I_fopen_sigset_file_new(parms->sigfile);
     if (fd == NULL) {
 	G_fatal_error(_("Unable to create signature file <%s>"),
 		      parms->sigfile);

+ 9 - 5
imagery/i.maxlik/i.maxlik.html

@@ -11,8 +11,10 @@ steps.  The first step in an unsupervised image
 classification is performed by
 <em><a href="i.cluster.html">i.cluster</a></em>; the first step in a
 supervised classification is executed by the GRASS
-program <em><a href="g.gui.iclass.html">g.gui.iclass</a></em>. In both
-cases, the second step in the image classification procedure is
+program <em><a href="g.gui.iclass.html">g.gui.iclass</a></em>
+(or by providing any other raster map with already existing
+training areas). In both cases, the second step in
+the image classification procedure is
 performed by <em>i.maxlik</em>.
 
 <p>
@@ -85,8 +87,8 @@ part of the example):
 
 <div class="code"><pre>
 # using here the signaturefile created by i.cluster
-i.maxlik group=lsat7_2002 subgroup=lsat7_2002 \
-  signaturefile=sig_cluster_lsat2002 \
+i.maxlik group=lsat7_2002 subgroup=res_30m \
+  signaturefile=cluster_lsat2002 \
   output=lsat7_2002_cluster_classes reject=lsat7_2002_cluster_reject
 
 # visually check result
@@ -146,11 +148,13 @@ Processing manual</a></em>
 Michael Shapiro,
 U.S.Army Construction Engineering 
 Research Laboratory
-
 <br>
 Tao Wen, 
 University of Illinois at Urbana-Champaign,
 Illinois
+<br>
+Band reference support: Maris Nartiss,
+University of Latvia
 
 <!--
 <p>

+ 3 - 3
imagery/i.maxlik/main.c

@@ -78,7 +78,7 @@ int main(int argc, char *argv[])
     parm.sigfile->key = "signaturefile";
     parm.sigfile->required = YES;
     parm.sigfile->type = TYPE_STRING;
-    parm.sigfile->gisprompt = "old,sig,sigfile";
+    parm.sigfile->gisprompt = "old,signatures/sig,sigfile";
     parm.sigfile->key_desc = "name";
     parm.sigfile->label = _("Name of input file containing signatures");
     parm.sigfile->description = _("Generated by either i.cluster, g.gui.iclass, or i.gensig");
@@ -101,10 +101,10 @@ int main(int argc, char *argv[])
     group = parm.group->answer;
     subgroup = parm.subgroup->answer;
     sigfile = parm.sigfile->answer;
-    
+
     if (G_unqualified_name(parm.class->answer, G_mapset(), class_name, xmapset) < 0)
         G_fatal_error(_("<%s> does not match the current mapset"), xmapset);
-    
+
     if (G_legal_filename(class_name) < 0)
         G_fatal_error(_("<%s> is an illegal file name"), class_name);
 

+ 21 - 13
imagery/i.maxlik/open.c

@@ -8,7 +8,7 @@
 
 int open_files(void)
 {
-    char *name, *mapset;
+    char *name, *mapset, **err;
     FILE *fd;
     int n;
 
@@ -31,18 +31,7 @@ int open_files(void)
 			    "The subgroup must have at least 2 raster maps."), subgroup, group);
     }
 
-    cell = (DCELL **) G_malloc(Ref.nfiles * sizeof(DCELL *));
-    cellfd = (int *)G_malloc(Ref.nfiles * sizeof(int));
-    P = (double *)G_malloc(Ref.nfiles * sizeof(double));
-    for (n = 0; n < Ref.nfiles; n++) {
-	cell[n] = Rast_allocate_d_buf();
-	name = Ref.file[n].name;
-	mapset = Ref.file[n].mapset;
-	cellfd[n] = Rast_open_old(name, mapset);
-    }
-
-    I_init_signatures(&S, Ref.nfiles);
-    fd = I_fopen_signature_file_old(group, subgroup, sigfile);
+    fd = I_fopen_signature_file_old(sigfile);
     if (fd == NULL)
 	G_fatal_error(_("Unable to open signature file <%s>"),
 		      sigfile);
@@ -57,9 +46,28 @@ int open_files(void)
 	G_fatal_error(_("<%s> has too many signatures (limit is 255)"),
 		      sigfile);
 
+    err = I_sort_signatures_by_bandref(&S, &Ref);
+    if (err)
+        G_fatal_error(_("Signature – group member band reference mismatch.\n"
+            "Extra signatures for bands: %s\n"
+            "Imagery group bands without signatures: %s"),
+            err[0] ? err[0] : _("none"),
+            err[1] ? err[1] : _("none")
+        );
+
     B = (double *)G_malloc(S.nsigs * sizeof(double));
     invert_signatures();
 
+    cell = (DCELL **) G_malloc(Ref.nfiles * sizeof(DCELL *));
+    cellfd = (int *)G_malloc(Ref.nfiles * sizeof(int));
+    P = (double *)G_malloc(Ref.nfiles * sizeof(double));
+    for (n = 0; n < Ref.nfiles; n++) {
+	cell[n] = Rast_allocate_d_buf();
+	name = Ref.file[n].name;
+	mapset = Ref.file[n].mapset;
+	cellfd[n] = Rast_open_old(name, mapset);
+    }
+
     class_fd = Rast_open_c_new(class_name);
     class_cell = Rast_allocate_c_buf();
 

+ 2 - 4
imagery/i.smap/bouman.h

@@ -1,6 +1,7 @@
 #include <math.h>
 #include <string.h>
 #include <grass/raster.h>
+#include <grass/imagery.h>
 
 #define LIKELIHOOD float
 
@@ -36,7 +37,7 @@ int parse(int, char *[], struct parms *);
 int closefiles(struct parms *, struct files *);
 
 /* openfiles.c */
-int openfiles(struct parms *, struct files *);
+int openfiles(struct parms *, struct files *, struct SigSet *);
 
 /* Suboutines in alpha_max.c */
 void alpha_max(double ***, double *, int, double);
@@ -61,9 +62,6 @@ int invert(double **, int);
 #ifdef GRASS_IMAGERY_H
 int segment(struct SigSet *, struct parms *, struct files *);
 
-/* read_sig.c */
-int read_signatures(struct parms *, struct SigSet *);
-
 /* labels.c */
 int create_output_labels(struct SigSet *, struct files *);
 

+ 36 - 16
imagery/i.smap/i.smap.html

@@ -103,13 +103,7 @@ categories on the ground.
 </dl>
 
 
-<h2>INTERACTIVE MODE</h2>
-
-If none of the arguments are specified on the command line,
-<em>i.smap</em> will interactively prompt for the names of
-the maps and files.
-
-<a name="notes"></a><h2>NOTES</h2>
+<h2 id="notes">NOTES</h2>
 
 The SMAP algorithm exploits the fact that nearby pixels in
 an image are likely to have the same class.  It works by
@@ -122,7 +116,7 @@ class which may be useful in some applications.
 
 <p>
 The amount of smoothing that is performed in the
-segmentation is dependent of the behavior of the data in
+segmentation is dependent of the behaviour of the data in
 the image.  If the data suggests that the nearby pixels
 often change class, then the algorithm will adaptively
 reduce the amount of smoothing.  This ensures that
@@ -144,24 +138,45 @@ r.mapcalc "MASKed_map = classification_results"
 
 <h2>EXAMPLE</h2>
 
-Supervised classification of LANDSAT
+Supervised classification of LANDSAT scene (complete NC location)
 
 <div class="code"><pre>
+## Prepare imagery for classification
+# Skip this step if your rasters already have band references defined
+# Define band references for all LANDSAT bands in mapset PERMANENT
+g.mapset mapset=PERMANENT
+r.support map=lsat7_2002_10 bandref=TM7_1
+r.support map=lsat7_2002_20 bandref=TM7_2
+r.support map=lsat7_2002_30 bandref=TM7_3
+r.support map=lsat7_2002_40 bandref=TM7_4
+r.support map=lsat7_2002_50 bandref=TM7_5
+r.support map=lsat7_2002_61 bandref=TM7_61
+r.support map=lsat7_2002_62 bandref=TM7_62
+r.support map=lsat7_2002_70 bandref=TM7_7
+r.support map=lsat7_2002_80 bandref=TM7_8
+g.mapset mapset=user1  # replace user1 with your mapset name
+
+## Real work starts here
+# Align computation region to the scene
 g.region raster=lsat7_2002_10 -p
 
 # store VIZ, NIR, MIR into group/subgroup
-i.group group=my_lsat7_2002 subgroup=my_lsat7_2002 \
+i.group group=lsat7_2002 subgroup=res_30m \
   input=lsat7_2002_10,lsat7_2002_20,lsat7_2002_30,lsat7_2002_40,lsat7_2002_50,lsat7_2002_70
 
 # Now digitize training areas "training" with the digitizer
 # and convert to raster model with v.to.rast
 v.to.rast input=training output=training use=cat label_column=label
+# If you are just playing around and do not care about the accuracy of outcome,
+# just use one of existing maps instead e.g.
+# g.copy rast=landuse96_28m,training
 
-# calculate statistics
-i.gensigset trainingmap=training group=my_lsat7_2002 subgroup=my_lsat7_2002 \
-            signaturefile=my_smap_lsat7_2002 maxsig=5
+# Create a signature file with statistics for each class
+i.gensigset trainingmap=training group=lsat7_2002 subgroup=res_30m \
+            signaturefile=lsat7_2002_30m maxsig=5
 
-i.smap group=my_lsat7_2002 subgroup=my_lsat7_2002 signaturefile=my_smap_lsat7_2002 \
+# Predict classes based on whole LANDSAT scene
+i.smap group=lsat7_2002 subgroup=res_30m signaturefile=lsat7_2002_30m \
        output=lsat7_2002_smap_classes
 
 # Visually check result
@@ -195,6 +210,9 @@ pp. III-565 - III-568, San Francisco, California, March 23-26, 1992.
 <h2>SEE ALSO</h2>
 
 <em>
+<a href="r.support.html">r.support</a></em> for setting band references,
+<br>
+<em>
 <a href="i.group.html">i.group</a></em> for creating groups and subgroups
 <br>
 <em><a href="r.mapcalc.html">r.mapcalc</a></em>
@@ -214,11 +232,13 @@ to generate the signature file required by this program
 
 <a href="https://engineering.purdue.edu/~bouman/software/segmentation/">Charles Bouman, 
 School of Electrical Engineering, Purdue University</a>
-
-<p>
+<br>
 Michael Shapiro,
 U.S.Army Construction Engineering 
 Research Laboratory
+<br>
+Band reference support: Maris Nartiss,
+University of Latvia
 
 <!--
 <p>

+ 1 - 2
imagery/i.smap/main.c

@@ -45,8 +45,7 @@ int main(int argc, char *argv[])
 	  "using sequential maximum a posteriori (SMAP) estimation.");
 
     parse(argc, argv, &parms);
-    openfiles(&parms, &files);
-    read_signatures(&parms, &S);
+    openfiles(&parms, &files, &S);
     create_output_labels(&S, &files);
 
     segment(&S, &parms, &files);

+ 25 - 1
imagery/i.smap/openfiles.c

@@ -8,10 +8,12 @@
 #include "local_proto.h"
 
 
-int openfiles(struct parms *parms, struct files *files)
+int openfiles(struct parms *parms, struct files *files, struct SigSet *S)
 {
+    FILE *fd;
     struct Ref Ref;		/* subgroup reference list */
     int n;
+    char **err;
 
 
     if (!I_get_subgroup_ref(parms->group, parms->subgroup, &Ref))
@@ -22,6 +24,28 @@ int openfiles(struct parms *parms, struct files *files)
 	G_fatal_error(_("Subgroup <%s> in group <%s> contains no raster maps"),
 		      parms->subgroup, parms->group);
 
+    fd = I_fopen_sigset_file_old(parms->sigfile);
+    if (fd == NULL)
+	G_fatal_error(_("Unable to read signature file <%s>"),
+		      parms->sigfile);
+
+    if (I_ReadSigSet(fd, S) < 0 || Ref.nfiles != S->nbands)
+	G_fatal_error(_("Signature file <%s> is invalid"), parms->sigfile);
+
+    if (S->ClassSig == NULL || S->title == NULL)
+	G_fatal_error(_("Signature file <%s> is empty"), parms->sigfile);
+
+    fclose(fd);
+
+    err = I_SortSigSetByBandref(S, &Ref);
+    if (err)
+        G_fatal_error(_("Signature – group member band reference mismatch.\n"
+            "Extra signatures for bands: %s\n"
+            "Imagery group bands without signatures: %s"),
+            err[0] ? err[0] : _("none"),
+            err[1] ? err[1] : _("none")
+        );
+
     /* allocate file descriptors, and io buffer */
     files->cellbuf = Rast_allocate_d_buf();
     files->outbuf = Rast_allocate_c_buf();

+ 1 - 1
imagery/i.smap/parse.c

@@ -20,7 +20,7 @@ int parse(int argc, char *argv[], struct parms *parms)
     sigfile->label = _("Name of input file containing signatures");
     sigfile->description = _("Generated by i.gensigset");
     sigfile->key_desc = "name";
-    sigfile->gisprompt = "old,sigset,sigfile";
+    sigfile->gisprompt = "old,signatures/sigset,sigfile";
     sigfile->required = YES;
     sigfile->type = TYPE_STRING;
 

+ 0 - 35
imagery/i.smap/read_sig.c

@@ -1,35 +0,0 @@
-#include <stdlib.h>
-#include <grass/imagery.h>
-#include <grass/glocale.h>
-#include "bouman.h"
-
-
-int read_signatures(struct parms *parms, struct SigSet *S)
-{
-    FILE *fd;
-    struct Ref Ref;
-
-    if (!I_get_subgroup_ref(parms->group, parms->subgroup, &Ref))
-	G_fatal_error(_("Unable to read REF file for subgroup <%s> in group <%s>"),
-		      parms->subgroup, parms->group);
-
-    if (Ref.nfiles <= 0)
-	G_fatal_error(_("Subgroup <%s> in group <%s> contains no raster maps"),
-		      parms->subgroup, parms->group);
-
-    fd = I_fopen_sigset_file_old(parms->group, parms->subgroup,
-				 parms->sigfile);
-    if (fd == NULL)
-	G_fatal_error(_("Unable to read signature file <%s>"),
-		      parms->sigfile);
-    
-    if (I_ReadSigSet(fd, S) < 0 || Ref.nfiles != S->nbands)
-	G_fatal_error(_("Signature file <%s> is invalid"), parms->sigfile);
-
-    if (S->ClassSig == NULL || S->title == NULL)
-	G_fatal_error(_("Signature file <%s> is empty"), parms->sigfile);
-
-    fclose(fd);
-
-    return 0;
-}

+ 6 - 0
imagery/imageryintro.html

@@ -181,6 +181,12 @@ Kappa statistic can be calculated to validate the results
 (<a href="r.kappa.html">r.kappa</a>). Covariance/correlation matrices can be
 calculated with <a href="r.covar.html">r.covar</a>.
 
+<p>
+Note - signatures generated for one scene are suitable for classification
+of other scenes as long as they consist of same raster bands
+(band references match). This comes handy when classifying multiple scenes
+from a single sensor taken in different areas or different times.
+
 <h3>Image fusion</h3>
 
 In case of using multispectral data, improvements of the resolution

+ 19 - 8
include/grass/defs/imagery.h

@@ -25,7 +25,8 @@ int I_find_subgroup(const char *, const char *);
 int I_find_subgroup2(const char *, const char *, const char *);
 int I_find_subgroup_file(const char *, const char *, const char *);
 int I_find_subgroup_file2(const char *, const char *, const char *, const char *);
-int I_find_signature_file(const char *, const char *, const char *, const char *);
+const char *I_find_signature(I_SIGFILE_TYPE, char *, const char *);
+const char *I_find_signature2(I_SIGFILE_TYPE, const char *, const char *);
 
 /* fopen.c */
 FILE *I_fopen_group_file_new(const char *, const char *);
@@ -94,7 +95,7 @@ void I_iclass_free_statistics(IClass_statistics *);
 /* iclass_signatures.c */
 int I_iclass_init_signatures(struct Signature *, struct Ref *);
 void I_iclass_add_signature(struct Signature *, IClass_statistics *);
-int I_iclass_write_signatures(struct Signature *, const char *, const char *, const char *);
+int I_iclass_write_signatures(struct Signature *, const char *);
 
 /* list_gp.c */
 int I_list_group(const char *, const struct Ref *, FILE *);
@@ -145,6 +146,15 @@ int I_merge_arrays(unsigned char *, unsigned char *, unsigned, unsigned, double)
 int I_apply_colormap(unsigned char *, unsigned char *, unsigned,  unsigned char *, unsigned char *);
 int I_rasterize(double *, int, unsigned char, struct Cell_head *, unsigned char *);
 
+/* manage_signatures.c */
+void I__get_signatures_element(char *, I_SIGFILE_TYPE);
+void I__make_signatures_element(I_SIGFILE_TYPE);
+int I_signatures_remove(I_SIGFILE_TYPE, const char *);
+int I_signatures_copy(I_SIGFILE_TYPE, const char *, const char *, const char *);
+int I_signatures_rename(I_SIGFILE_TYPE, const char *, const char *);
+int I_signatures_list_by_type(I_SIGFILE_TYPE, const char *, char ***);
+void I_free_signatures_list(int, char ***);
+
 /* sig.c */
 int I_init_signatures(struct Signature *, int);
 int I_new_signature(struct Signature *);
@@ -152,16 +162,16 @@ int I_free_signatures(struct Signature *);
 int I_read_one_signature(FILE *, struct Signature *);
 int I_read_signatures(FILE *, struct Signature *);
 int I_write_signatures(FILE *, struct Signature *);
+char **I_sort_signatures_by_bandref(struct Signature *, const struct Ref *);
 
 /* sigfile.c */
-FILE *I_fopen_signature_file_new(const char *, const char *, const char *);
-FILE *I_fopen_signature_file_old(const char *, const char *, const char *);
+FILE *I_fopen_signature_file_new(const char *);
+FILE *I_fopen_signature_file_old(const char *);
 
 /* sigset.c */
 int I_SigSetNClasses(struct SigSet *);
 struct ClassData *I_AllocClassData(struct SigSet *, struct ClassSig *, int);
-int I_InitSigSet(struct SigSet *);
-int I_SigSetNBands(struct SigSet *, int);
+int I_InitSigSet(struct SigSet *, int);
 struct ClassSig *I_NewClassSig(struct SigSet *);
 struct SubSig *I_NewSubSig(struct SigSet *, struct ClassSig *);
 int I_ReadSigSet(FILE *, struct SigSet *) WARN_UNUSED_RESULT;
@@ -170,10 +180,11 @@ const char *I_GetSigTitle(const struct SigSet *);
 int I_SetClassTitle(struct ClassSig *, const char *);
 const char *I_GetClassTitle(const struct ClassSig *);
 int I_WriteSigSet(FILE *, const struct SigSet *);
+char **I_SortSigSetByBandref(struct SigSet *, const struct Ref *);
 
 /* sigsetfile.c */
-FILE *I_fopen_sigset_file_new(const char *, const char *, const char *);
-FILE *I_fopen_sigset_file_old(const char *, const char *, const char *);
+FILE *I_fopen_sigset_file_new(const char *);
+FILE *I_fopen_sigset_file_old(const char *);
 
 /* target.c */
 int I_get_target(const char *, char *, char *);

+ 27 - 13
include/grass/imagery.h

@@ -51,8 +51,8 @@ struct Control_Points
 
 struct One_Sig
 {
-    char desc[100];
-    int npoints;
+    char desc[100];     /* name of target class */
+    int npoints;        /* cell count used to determine class parameters */
     double *mean;		/* one mean for each band */
     double **var;		/* covariance band-band   */
     int status;		/* may be used to 'delete' a signature */
@@ -62,10 +62,11 @@ struct One_Sig
 
 struct Signature
 {
-    int nbands;
-    int nsigs;
-    char title[100];
-    struct One_Sig *sig;
+    int nbands;         /* band (imagery group member) count */
+    char **bandrefs;    /* list of band references */
+    int nsigs;          /* signature count */
+    char title[100];    /* not used? */
+    struct One_Sig *sig;    /* array of one signature per class */
 };
 
 struct SubSig
@@ -89,19 +90,20 @@ struct ClassData
 
 struct ClassSig
 {
-    long classnum;
-    char *title;
+    long classnum;      /* c_cat */
+    char *title;        /* from Rast_get_c_cat */
     int used;
-    int type;
-    int nsubclasses;
+    int type;           /* always is SIGNATURE_TYPE_MIXED ? */
+    int nsubclasses;    /* SubSig item count */
     struct SubSig *SubSig;
-    struct ClassData ClassData;
+    struct ClassData ClassData; /* used for SubSig calculation only */
 };
 
 struct SigSet
 {
     int nbands;
-    int nclasses;
+    char **bandrefs;    /* list of band references [nbands]char* */
+    int nclasses;       /* ClassSig item count */
     char *title;
     struct ClassSig *ClassSig;
 };
@@ -194,7 +196,19 @@ struct scdScattData
                                         (used for SC_SCATT_DATA type) otherwise NULL */
 };
 
-#define SIGNATURE_TYPE_MIXED 1
+/*! Supported signature file types.
+ *  Remember to adjust I_SIGFILE_TYPE_COUNT on a change
+ */
+typedef enum
+{
+    I_SIGFILE_TYPE_SIG,       /*! Signature files used by i.maxlik */
+    I_SIGFILE_TYPE_SIGSET,    /*! Signature files used by i.smap */
+
+} I_SIGFILE_TYPE;
+
+#define SIGNATURE_TYPE_MIXED 1  /* Unused? */
+#define I_SIGFILE_TYPE_COUNT 2  /*! Total count of supported signature file types */
+
 
 #define GROUPFILE "CURGROUP"
 #define SUBGROUPFILE "CURSUBGROUP"

+ 58 - 29
lib/imagery/find.c

@@ -176,36 +176,65 @@ int I_find_subgroup_file2(const char *group, const char *subgroup,
 }
 
 /*!
- * \brief does signature file exists?
+ * \brief Find mapset containing signature file
  *
- * Returns 1 if the
- * specified <b>signature</b> exists in the specified subgroup; 0 otherwise.
- * 
- * Should be used to check if signature file exists after G_parser run
- * when generating new signature file.
+ * Looks for the signature <i>name</i> of type <i>type</i>
+ * in the database. The <i>mapset</i> parameter can either be
+ * the empty string "", which means search all the mapsets in
+ * the users current mapset search path
+ * (see \ref Mapset_Search_Path for more details about the search
+ * path) or it can be a specific mapset name, which means look for the
+ * signature only in this one mapset (for example, in the current
+ * mapset). If found, the mapset where the signature lives is
+ * returned. If not found, the NULL pointer is returned.
  *
- *  \param group - group where to search
- *  \param subgroup - subgroup containing signatures
- *  \param type - type of signature ("sig" or "sigset")
- *  \param file - name of signature file
- *  \return int
+ * Note: If the user specifies a fully qualified signature name which
+ * exists, then I_find_signature() modifies <i>name</i> by removing
+ * the "@<i>mapset</i>".
+ * Use I_find_signature2 if altering passed in name is not desired.
+ *
+ * \param type I_SIGFILE_TYPE
+ * \param name of signature
+ * \param mapset set NULL to search in all mapsets
+ * \return mapset or NULL
  */
-int I_find_signature_file(const char *group, const char *subgroup,
-                     const char *type, const char *file)
-{
-    char element[GNAME_MAX * 2];
-    
-    if (!I_find_group(group))
-        return 0;
-    if (subgroup == NULL || *subgroup == 0)
-        return 0;
-    if (type == NULL || *type == 0)
-        return 0;
-    if (file == NULL || *file == 0)
-        return 0;
-        
-    sprintf(element, "subgroup%c%s%c%s%c%s", HOST_DIRSEP, subgroup, HOST_DIRSEP, type, HOST_DIRSEP, file);
-    G_debug(5, "I_find_signature_file() element: %s", element);
-    
-    return G_find_file2_misc("group", element, group, G_mapset()) != NULL;
+const char *I_find_signature(I_SIGFILE_TYPE type, char *name, const char *mapset) {
+    char selem[GNAME_MAX]; /* 'signatures/type\0' */
+
+    G_debug(1, "I_find_signature(): type=%d name=%s mapset=%s", type, name, mapset);
+
+    I__get_signatures_element(selem, type);
+
+    return G_find_file(selem, name, mapset);
+}
+
+/*!
+ * \brief Find mapset containing signature (look but don't touch)
+ *
+ * Looks for the signature <i>name</i> of type <i>type</i>
+ * in the database. The <i>mapset</i> parameter can either be
+ * the empty string "", which means search all the mapsets in
+ * the users current mapset search path
+ * (see \ref Mapset_Search_Path for more details about the search
+ * path) or it can be a specific mapset name, which means look for the
+ * signature only in this one mapset (for example, in the current
+ * mapset). If found, the mapset where the signature lives is
+ * returned. If not found, the NULL pointer is returned.
+ *
+ * Note: The passed name argument is not altered.
+ * Use I_find_signature if stripping mapset part from the name is desired.
+ *
+ * \param type I_SIGFILE_TYPE
+ * \param name of signature
+ * \param mapset set NULL to search in all mapsets
+ * \return mapset or NULL
+ */
+const char *I_find_signature2(I_SIGFILE_TYPE type, const char *name, const char *mapset) {
+    char selem[GNAME_MAX]; /* 'signatures/type\0' */
+
+    G_debug(1, "I_find_signature2(): type=%d name=%s mapset=%s", type, name, mapset);
+
+    I__get_signatures_element(selem, type);
+
+    return G_find_file2(selem, name, mapset);
 }

+ 13 - 10
lib/imagery/iclass_signatures.c

@@ -48,10 +48,17 @@ int I_iclass_init_signatures(struct Signature *sigs, struct Ref *refer)
 {
     G_debug(3, "I_iclass_init_signatures()");
 
-    if (!I_init_signatures(sigs, refer->nfiles))
-	return 1;		/* success */
+    I_init_signatures(sigs, refer->nfiles);
+    for (unsigned int i = refer->nfiles; i--;) {
+        sigs->bandrefs[i] = Rast_read_bandref(refer->file[i].name, refer->file[i].mapset);
+        if (!sigs->bandrefs[i]) {
+            G_warning(_("Raster map <%s@%s> lacks band reference"),
+                refer->file[i].name, refer->file[i].mapset);
+            return 0;
+        }
+    }
 
-    return 0;
+    return 1;
 }
 
 /*!
@@ -99,24 +106,20 @@ void I_iclass_add_signature(struct Signature *sigs,
    \brief Write signtures to signature file.
 
    \param sigs pointer to signatures
-   \param group image group
-   \param sub_group image subgroup
    \param file_name name of signature file
 
    \return 1 on success
    \return 0 on failure
  */
-int I_iclass_write_signatures(struct Signature *sigs, const char *group,
-			      const char *sub_group, const char *file_name)
+int I_iclass_write_signatures(struct Signature *sigs, const char *file_name)
 {
     FILE *outsig_fd;
 
-    G_debug(3, "I_write_signatures(): group=%s, file_name=%s", group,
-	    file_name);
+    G_debug(3, "I_write_signatures(): file_name=%s", file_name);
 
     if (!
 	(outsig_fd =
-	 I_fopen_signature_file_new(group, sub_group, file_name))) {
+	 I_fopen_signature_file_new(file_name))) {
 	G_warning(_("Unable to open output signature file '%s'"), file_name);
 	return 0;
     }

+ 292 - 0
lib/imagery/manage_signatures.c

@@ -0,0 +1,292 @@
+/*!
+   \file lib/imagery/manage_sinatures.c
+
+   \brief Imagery Library - Signature file management functions
+
+   (C) 2021 by 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.
+
+   \author Maris Nartiss
+ */
+
+#include <unistd.h>
+#include <string.h>
+
+#include <grass/gis.h>
+#include <grass/imagery.h>
+#include <grass/glocale.h>
+
+/*!
+  \brief Get signature element (internal use only)
+
+  \param element [GNAME_MAX] allocated string buffer
+  \param type I_SIGFILE_TYPE
+*/
+void I__get_signatures_element(char *element, I_SIGFILE_TYPE type)
+{
+    if (type == I_SIGFILE_TYPE_SIG) {
+        sprintf(element, "signatures%csig", HOST_DIRSEP);
+    }
+    else if (type == I_SIGFILE_TYPE_SIGSET) {
+        sprintf(element, "signatures%csigset", HOST_DIRSEP);
+    }
+    else {
+        G_fatal_error("Programming error: unknown signature file type");
+    }
+}
+
+/*!
+  \brief Make signature element (internal use only)
+
+  \param type I_SIGFILE_TYPE
+*/
+void I__make_signatures_element(I_SIGFILE_TYPE type)
+{
+    char element[GNAME_MAX];
+    G_make_mapset_object_group("signatures");
+    I__get_signatures_element(element, type);
+    G_make_mapset_object_group(element);
+}
+
+static int list_by_type(I_SIGFILE_TYPE, const char *, int, char ***);
+
+/*!
+ * \brief Remove a signature file
+ * 
+ * If removal fails, prints a warning and returns 1.
+ * It is safe to pass fully qualified names.
+ * 
+ * \param type I_SIGFILE_TYPE signature type
+ * \param name of signature to remove
+ * \return 0 on success
+ * \return 1 on failure
+ */
+int I_signatures_remove(I_SIGFILE_TYPE type, const char *name)
+{
+    char xname[GNAME_MAX], xmapset[GMAPSET_MAX];
+    char element[GNAME_MAX];
+    int ret = 0;
+
+    G_debug(1, "I_signatures_remove(%d, %s);", type, name);
+
+    /* Remove only if file is in the current mapset */
+    if (G_name_is_fully_qualified(name, xname, xmapset) &&
+        strcmp(xmapset, G_mapset()) != 0) {
+        G_warning(_("%s is not in the current mapset (%s)"), name,
+                  G_mapset());
+        return 1;
+    }
+    if (I_find_signature2(type, name, G_mapset())) {
+        I__get_signatures_element(element, type);
+        if (G_remove(element, name) == 1) {
+            G_verbose_message(_("%s removed"), name);
+            return 0;
+        }
+        G_warning(_("Unable to remove %s signature"), name);
+    }
+    else
+        G_warning(_("%s is missing"), name);
+    return 1;
+}
+
+/*!
+ * \brief Copy a signature file
+ *
+ * If copy fails, prints warning messages and returns 1.
+ * It is safe to pass fully qualified names.
+ *
+ * \param type I_SIGFILE_TYPE signature type
+ * \param name of old signature
+ * \param mapset of old signature
+ * \param name of new signature
+ * \return 0 on success
+ * \return 1 on failure
+ */
+int I_signatures_copy(I_SIGFILE_TYPE type, const char *old_name,
+                      const char *old_mapset, const char *new_name)
+{
+    char sname[GNAME_MAX], tname[GNAME_MAX], tmapset[GMAPSET_MAX],
+        xmapset[GMAPSET_MAX];
+    char element[GNAME_MAX];
+    const char *smapset;
+    char old_path[GPATH_MAX], new_path[GPATH_MAX];
+
+    G_debug(1, "I_signatures_copy(%d, %s@%s, %s);", type, old_name,
+            old_mapset, new_name);
+
+    /* Copy only if mapset of new name is the current mapset */
+    if (G_name_is_fully_qualified(new_name, tname, tmapset)) {
+        if (strcmp(tmapset, G_mapset()) != 0) {
+            G_warning(_("%s is not in the current mapset (%s)"), new_name,
+                      G_mapset());
+            return 1;
+        }
+    }
+    else
+        strcat(tname, new_name);
+
+    smapset = I_find_signature2(type, old_name, old_mapset);
+    if (!smapset) {
+        G_warning(_("%s is missing"), old_name);
+        return 1;
+    }
+    G_unqualified_name(old_name, NULL, sname, xmapset);
+
+    I__make_signatures_element(type);
+
+    I__get_signatures_element(element, type);
+    G_file_name(old_path, element, sname, smapset);
+    G_file_name(new_path, element, tname, G_mapset());
+
+    if (G_copy_file(old_path, new_path) != 1) {
+        G_warning(_("Unable to copy <%s> to current mapset as <%s>"),
+                  G_fully_qualified_name(old_name, smapset), tname);
+        return 1;
+    }
+    return 0;
+}
+
+/*!
+ * \brief Rename a signature file
+ *
+ * If rename fails, prints warning messages and returns 1.
+ * It is safe to pass fully qualified names.
+ *
+ * \param type I_SIGFILE_TYPE signature type
+ * \param name of old signature
+ * \param name of new signature
+ * \return 0 on success
+ * \return 1 on failure
+ */
+int I_signatures_rename(I_SIGFILE_TYPE type, const char *old_name,
+                        const char *new_name)
+{
+    char sname[GNAME_MAX], tname[GNAME_MAX], tmapset[GMAPSET_MAX];
+    char element[GNAME_MAX];
+    const char *smapset;
+    char old_path[GPATH_MAX], new_path[GPATH_MAX];
+
+    G_debug(1, "I_signatures_rename(%d, %s, %s);", type, old_name, new_name);
+
+    /* Rename only if source and destination mapset is the current mapset */
+    if (G_name_is_fully_qualified(old_name, sname, tmapset)) {
+        if (strcmp(tmapset, G_mapset()) != 0) {
+            G_warning(_("%s is not in the current mapset (%s)"), old_name,
+                      G_mapset());
+            return 1;
+        }
+    }
+    else
+        strcat(sname, old_name);
+    if (G_name_is_fully_qualified(new_name, tname, tmapset)) {
+        if (strcmp(tmapset, G_mapset()) != 0) {
+            G_warning(_("%s is not in the current mapset (%s)"), new_name,
+                      G_mapset());
+            return 1;
+        }
+    }
+    else
+        strcat(tname, new_name);
+
+    smapset = I_find_signature2(type, old_name, tmapset);
+    if (!smapset) {
+        G_warning(_("%s is missing"), old_name);
+        return 1;
+    }
+
+    I__get_signatures_element(element, type);
+    G_file_name(old_path, element, sname, tmapset);
+    G_file_name(new_path, element, tname, tmapset);
+
+    if (G_rename_file(old_path, new_path) != 0) {
+        G_warning(_("Unable to rename <%s> to <%s>"), old_name, new_name);
+        return 1;
+    }
+    return 0;
+}
+
+/*!
+ * \brief Get list of existing signatures by type
+ *
+ * Fills passed list with fully qualified names of existing signatures.
+ *
+ * If no mapset is passed, all mapsets in the search path are used.
+ * If no signatures are found, returns 0 and list is set to NULL.
+ *
+ * The function will assign memory for the list. It is up to callee to
+ * free the memory of each list item and the list itself.
+ *
+ * \param type I_SIGFILE_TYPE signature type
+ * \param mapset optional mapset to search in or NULL
+ * \param pointer to array of found signature strings or NULL if none found
+ * \return count of signature strings in the array
+ */
+int I_signatures_list_by_type(I_SIGFILE_TYPE type, const char *mapset,
+                              char ***out_list)
+{
+    int base = 0;
+
+    *out_list = NULL;
+    if (mapset == NULL) {
+        for (int n = 0; (mapset = G_get_mapset_name(n)); n++) {
+            base += list_by_type(type, mapset, base, out_list);
+        }
+    }
+    else {
+        base += list_by_type(type, mapset, base, out_list);
+    }
+
+    return base;
+}
+
+/*!
+ * \brief Free memory allocated by I_signatures_list_by_type
+ *
+ * Calls G_free for all list items returned by I_signatures_list_by_type()
+ *
+ * \param int Return value of I_signatures_list_by_type()
+ * \param pointer to array filled by I_signatures_list_by_type()
+ */
+void I_free_signatures_list(int count, char ***list)
+{
+    for (int n = 0; n < count; n++) {
+        G_free((*list)[n]);
+    }
+    G_free(*list);
+}
+
+static int list_by_type(I_SIGFILE_TYPE type, const char *mapset, int base,
+                        char ***out_list)
+{
+    int count = 0;
+    char path[GPATH_MAX];
+    char element[GNAME_MAX];
+    char **dirlist;
+
+    I__get_signatures_element(element, type);
+    G_file_name(path, element, "", mapset);
+
+    if (access(path, 0) != 0) {
+        return count;
+    }
+
+    dirlist = G_ls2(path, &count);
+    if (count == 0)
+        return count;
+
+    /* Make items fully qualified names */
+    int mapset_len = strlen(mapset);
+
+    *out_list =
+        (char **)G_realloc(*out_list, (base + count) * sizeof(char *));
+    for (int i = 0; i < count; i++) {
+        (*out_list)[base + i] =
+            (char *)G_malloc((strlen(dirlist[i]) + 1 + mapset_len + 1) *
+                             sizeof(char));
+        sprintf((*out_list)[base + i], "%s@%s", dirlist[i], mapset);
+    }
+
+    return count;
+}

+ 315 - 9
lib/imagery/sig.c

@@ -1,9 +1,22 @@
 #include <stdlib.h>
+#include <string.h>
 #include <grass/imagery.h>
+#include <grass/glocale.h>
 
+/*!
+ * \brief Initialize struct Signature before use
+ *
+ * No need to call before calling I_read_signatures.
+ *
+ * \param *Signature to initialize
+ * \param nbands band (imagery group member) count
+ */
 int I_init_signatures(struct Signature *S, int nbands)
 {
     S->nbands = nbands;
+    S->bandrefs = (char **)G_malloc(nbands * sizeof(char **));
+    for (int i = 0; i < nbands; i++)
+        S->bandrefs[i] = NULL;
     S->nsigs = 0;
     S->sig = NULL;
     S->title[0] = 0;
@@ -29,10 +42,20 @@ int I_new_signature(struct Signature *S)
 
     S->sig[i].status = 0;
     S->sig[i].have_color = 0;
-    sprintf(S->sig[i].desc, "Class %d", i + 1);;
+    sprintf(S->sig[i].desc, "Class %d", i + 1);
     return S->nsigs;
 }
 
+/*!
+ * \brief Free memory allocated for struct Signature
+ *
+ * One must call I_init_signatures() to re-use struct after it has been
+ * passed to this function.
+ *
+ * \param *Signature to free
+ *
+ * \return always 0
+ */
 int I_free_signatures(struct Signature *S)
 {
     int n;
@@ -44,7 +67,16 @@ int I_free_signatures(struct Signature *S)
 	free(S->sig[i].var);
 	free(S->sig[i].mean);
     }
-    I_init_signatures(S, 0);
+    free(S->sig);
+    for (n = 0; n < S->nbands; n++)
+        free(S->bandrefs[n]);
+    free(S->bandrefs);
+
+    S->sig = NULL;
+    S->bandrefs = NULL;
+    S->nbands = 0;
+    S->nsigs = 0;
+    S->title[0] = '\0';
 
     return 0;
 }
@@ -91,19 +123,87 @@ int I_read_one_signature(FILE * fd, struct Signature *S)
     return 1;
 }
 
+/*!
+ * \brief Read signatures from file
+ *
+ * File stream should be opened in advance by call to
+ * I_fopen_signature_file_old()
+ * It is up to caller to fclose the file stream afterwards.
+ *
+ * There is no need to initialize struct Signature in advance, as this
+ * function internally calls I_init_signatures.
+ *
+ * \param pointer to FILE*
+ * \param pointer to struct Signature *S
+ *
+ * \return 1 on success, -1 on failure
+ */
 int I_read_signatures(FILE * fd, struct Signature *S)
 {
-    int n;
+    int ver, n, pos;
+    char c, prev;
+    char bandref[GNAME_MAX];
 
+    I_init_signatures(S, 0);
     S->title[0] = 0;
-    while ((n = fgetc(fd)) != EOF)
-	if (n == '#')
-	    break;
-    if (n != '#')
-	return -1;
+    /* File of signatures must start with its version number */
+    if (fscanf(fd, "%d", &ver) != 1) {
+        G_warning(_("Invalid signature file"));
+        return -1;
+    }
+    /* Current version number is 1 */
+    if (ver != 1) {
+        G_warning(_("Invalid signature file version"));
+        return -1;
+    }
+
+    /* Goto title line and strip initial # */
+    while ((c = (char)fgetc(fd)) != EOF)
+        if (c == '#')
+            break;
     I_get_to_eol(S->title, sizeof(S->title), fd);
     G_strip(S->title);
 
+    /* Read band references and count them to set nbands */
+    n = 0;
+    pos = 0;
+    S->bandrefs = (char **)G_realloc(S->bandrefs, (n + 1) * sizeof(char **));
+    while ((c = (char)fgetc(fd)) != EOF) {
+        if (c == '\n') {
+            if (prev != ' ') {
+                bandref[pos] = '\0';
+                S->bandrefs[n] = G_store(bandref);
+                n++;
+            }
+            S->nbands = n;
+            break;
+        }
+        if (c == ' ') {
+            bandref[pos] = '\0';
+            S->bandrefs[n] = G_store(bandref);
+            n++;
+            /* [n] is 0 based thus: (n + 1) */
+            S->bandrefs = (char **)G_realloc(S->bandrefs, (n + 1) * sizeof(char **));
+            pos = 0;
+            prev = c;
+            continue;
+        }
+        /* Band references are limited to GNAME_MAX - 1 + \0 in length;
+         * n is 0-based */
+        if (pos == (GNAME_MAX - 2)) {
+            G_warning(_("Invalid signature file: band reference length limit exceeded"));
+            return -1;
+        }
+        bandref[pos] = c;
+        pos++;
+        prev = c;
+    }
+
+    if (!(S->nbands > 0)) {
+        G_warning(_("Signature file does not contain bands"));
+        return -1;
+    }
+
     while ((n = I_read_one_signature(fd, S)) == 1) ;
 
     if (n < 0)
@@ -113,6 +213,18 @@ int I_read_signatures(FILE * fd, struct Signature *S)
     return 1;
 }
 
+/*!
+ * \brief Write signatures to file
+ *
+ * File stream should be opened in advance by call to
+ * I_fopen_signature_file_new()
+ * It is up to caller to fclose the file stream afterwards.
+ *
+ * \param pointer to FILE*
+ * \param pointer to struct Signature *S
+ *
+ * \return always 1
+ */
 int I_write_signatures(FILE * fd, struct Signature *S)
 {
     int k;
@@ -120,13 +232,28 @@ int I_write_signatures(FILE * fd, struct Signature *S)
     int i;
     struct One_Sig *s;
 
+    /* Version of signatures file structure.
+     * Increment if file structure changes.
+     */
+    fprintf(fd, "1\n");
+    /* Title of signatures */
     fprintf(fd, "#%s\n", S->title);
+    /* A list of space separated band references for each
+     * raster map used to generate sigs. */
+    for (k = 0; k < S->nbands; k++) {
+        fprintf(fd, "%s ", S->bandrefs[k]);
+    }
+    fprintf(fd, "\n");
+    /* A signature for each target class */
     for (k = 0; k < S->nsigs; k++) {
 	s = &S->sig[k];
 	if (s->status != 1)
 	    continue;
-	fprintf(fd, "#%s\n", s->desc);
+    /* Label for each class represented by this signature */
+    fprintf(fd, "#%s\n", s->desc);
+    /* Point count used to generate signature */
 	fprintf(fd, "%d\n", s->npoints);
+    /* Values are in the same order as band references */
 	for (i = 0; i < S->nbands; i++)
 	    fprintf(fd, "%g ", s->mean[i]);
 	fprintf(fd, "\n");
@@ -140,3 +267,182 @@ int I_write_signatures(FILE * fd, struct Signature *S)
     }
     return 1;
 }
+
+/*!
+ * \brief Reorder struct Signature to match imagery group member order
+ *
+ * The function will check for band reference match between signature struct
+ * and imagery group.
+ *
+ * In the case of a complete band reference match, values of passed in
+ * struct Signature are reordered to match the order of imagery group items.
+ *
+ * If all band references are not identical (in
+ * arbitrary order), function will return two dimensional array with
+ * comma separated list of:
+ *      - [0] band references present in the signature struct but
+ * absent in the imagery group
+ *      - [1] band references present in the imagery group but
+ * absent in the signature struct
+ *
+ * If no mismatch of band references for signatures or imagery group are
+ * detected (== all are present in the other list), a NULL value will be
+ * returned in the particular list of mismatches (not an empty string).
+ * For example:
+ * \code if (ret && ret[1]) printf("List of imagery group bands without signatures: %s\n, ret[1]); \endcode
+ *
+ * \param *Signature existing signatures to check & sort
+ * \param *Ref group reference
+ *
+ * \return NULL successfully sorted
+ * \return err_array two comma separated lists of mismatches
+ */
+char **I_sort_signatures_by_bandref(struct Signature *S, const struct Ref *R) {
+    unsigned int total, complete;
+    unsigned int *match1, *match2, mc1, mc2, *new_order;
+    double **new_means, ***new_vars;
+    char **group_bandrefs, **mismatches, **new_bandrefs;
+
+    /* Safety measure. Untranslated as this should not happen in production! */
+    if (S->nbands < 1 || R->nfiles < 1)
+        G_fatal_error("Programming error. Invalid length structs passed to "
+                      "I_sort_signatures_by_bandref(%d, %d);", S->nbands,  R->nfiles);
+
+    /* Obtain group band references */
+    group_bandrefs = (char **)G_malloc(R->nfiles * sizeof(char *));
+    for (unsigned int j = R->nfiles; j--;) {
+        group_bandrefs[j] = Rast_read_bandref(R->file[j].name, R->file[j].mapset);
+    }
+
+    /* If lengths are not equal, there will be a mismatch */
+    complete = S->nbands == R->nfiles;
+
+    /* Initialize match tracker */
+    new_order = (unsigned int *)G_malloc(S->nbands * sizeof(unsigned int));
+    match1 = (unsigned int *)G_calloc(S->nbands, sizeof(unsigned int));
+    match2 = (unsigned int *)G_calloc(R->nfiles, sizeof(unsigned int));
+
+    /* Allocate memory for temporary storage of sorted values */
+    new_bandrefs = (char **)G_malloc(S->nbands * sizeof(char *));
+    new_means = (double **)G_malloc(S->nsigs * sizeof(double *));
+    // new_vars[S.sig[x]][band1][band1]
+    new_vars = (double ***)G_malloc(S->nsigs * sizeof(double **));
+    for (unsigned int c = S->nsigs; c--;) {
+        new_means[c] = (double *)G_malloc(S->nbands * sizeof(double));
+        new_vars[c] = (double **)G_malloc(S->nbands * sizeof(double *));
+        for (unsigned int i = S->nbands; i--;)
+            new_vars[c][i] = (double *)G_malloc(S->nbands * sizeof(double));
+    }
+
+    /* Obtain order of matching items */
+    for (unsigned int j = R->nfiles; j--;) {
+        for (unsigned int i = S->nbands; i--;) {
+            if (S->bandrefs[i] && group_bandrefs[j] &&
+                !strcmp(S->bandrefs[i], group_bandrefs[j])) {
+                    if (complete) {
+                        /* Reorder pointers to existing strings only */
+                        new_bandrefs[j] = S->bandrefs[i];
+                        new_order[i] = j;
+                    }
+                    /* Keep a track of matching items for error reporting */
+                    match1[i] = 1;
+                    match2[j] = 1;
+                    break;
+            }
+        }
+    }
+
+    /* Check for band reference mismatch */
+    mc1 = mc2 = 0;
+    mismatches = (char **)G_malloc(2 * sizeof(char **));
+    mismatches[0] = NULL;
+    mismatches[1] = NULL;
+    total = 1;
+    for (unsigned int i = 0; i < S->nbands; i++) {
+        if (!match1[i]) {
+            if (S->bandrefs[i])
+                total = total + strlen(S->bandrefs[i]);
+            else
+                total = total + 24;
+            mismatches[0] = (char *)G_realloc(mismatches[0], total * sizeof(char *));
+            if (mc1)
+                strcat(mismatches[0], ",");
+            else
+                mismatches[0][0] = '\0';
+            if (S->bandrefs[i])
+                strcat(mismatches[0], S->bandrefs[i]);
+            else
+                strcat(mismatches[0], "<band reference missing>");
+            mc1++;
+            total = total + 1;
+        }
+    }
+    total = 1;
+    for (unsigned int j = 0; j < R->nfiles; j++) {
+        if (!match2[j]) {
+            if (group_bandrefs[j])
+                total = total + strlen(group_bandrefs[j]);
+            else
+                total = total + 24;
+            mismatches[1] = (char *)G_realloc(mismatches[1], total * sizeof(char *));
+            if (mc2)
+                strcat(mismatches[1], ",");
+            else
+                mismatches[1][0] = '\0';
+            if (group_bandrefs[j])
+                strcat(mismatches[1], group_bandrefs[j]);
+            else
+                strcat(mismatches[1], "<band reference missing>");
+            mc2++;
+            total = total + 1;
+        }
+    }
+
+    /* Swap var matrix values in each of classes */
+    if (!mc1 && !mc2) {
+        for (unsigned int c = S->nsigs; c--;) {
+            for (unsigned int b1 = 0; b1 < S->nbands; b1++) {
+                new_means[c][new_order[b1]] = S->sig[c].mean[b1];
+                for (unsigned int b2 = 0; b2 <= b1; b2++) {
+                    if (new_order[b1] > new_order[b2]) {
+                        new_vars[c][new_order[b1]][new_order[b2]] = S->sig[c].var[b1][b2];
+                    }
+                    else {
+                        new_vars[c][new_order[b2]][new_order[b1]] = S->sig[c].var[b1][b2];
+                    }
+                }
+            }
+        }
+
+        /* Replace values in struct with ordered ones */
+        memcpy(S->bandrefs, new_bandrefs, S->nbands * sizeof(char **));
+        for (unsigned int c = S->nsigs; c--;) {
+            memcpy(S->sig[c].mean, new_means[c], S->nbands * sizeof(double));
+            for (unsigned int i = S->nbands; i--;)
+                memcpy(S->sig[c].var[i], new_vars[c][i], S->nbands * sizeof(double));
+        }
+    }
+
+    /* Clean up */
+    for (unsigned int j = R->nfiles; j--;)
+        free(group_bandrefs[j]);
+    free(group_bandrefs);
+    free(new_order);
+    free(match1);
+    free(match2);
+    free(new_bandrefs);
+    for (unsigned int c = S->nsigs; c--;) {
+        free(new_means[c]);
+        for (unsigned int i = S->nbands; i--;)
+            free(new_vars[c][i]);
+        free(new_vars[c]);
+    }
+    free(new_means);
+    free(new_vars);
+
+    if (mc1 || mc2) {
+        return mismatches;
+    }
+    free(mismatches);
+    return NULL;
+}

+ 26 - 36
lib/imagery/sigfile.c

@@ -1,15 +1,15 @@
 /*!
    \file lib/imagery/sigfile.c
-   
+
    \brief Imagery Library - Signature file functions (statistics for i.maxlik).
- 
-   (C) 2001-2008, 2013 by the GRASS Development Team
-   
+
+   (C) 2001-2008, 2013, 2021 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 USA CERL
-*/
+ */
 
 #include <string.h>
 #include <grass/imagery.h>
@@ -17,56 +17,46 @@
 /*!
    \brief Create signature file
 
-   \param group group name
-   \param subgroup subgroup name in given group
    \param name signature filename
 
-   \return pointer to FILE*
+   \return pointer to FILE
    \return NULL on error
-*/
-FILE *I_fopen_signature_file_new(const char *group,
-				 const char *subgroup, const char *name)
+ */
+FILE *I_fopen_signature_file_new(const char *name)
 {
-    char element[GPATH_MAX];
-    char group_name[GNAME_MAX], group_mapset[GMAPSET_MAX];
+    char element[GNAME_MAX];
     FILE *fd;
 
-    if (!G_name_is_fully_qualified(group, group_name, group_mapset)) {
-	strcpy(group_name, group);
-    }
+    /* create sig directory */
+    I__make_signatures_element(I_SIGFILE_TYPE_SIG);
 
-    /* create sigset directory */
-    sprintf(element, "%s/subgroup/%s/sig", group_name, subgroup);
-    G__make_mapset_element_misc("group", element);
+    I__get_signatures_element(element, I_SIGFILE_TYPE_SIG);
+    fd = G_fopen_new(element, name);
 
-    sprintf(element, "subgroup/%s/sig/%s", subgroup, name);
-
-    fd = G_fopen_new_misc("group", element, group_name);
-    
     return fd;
 }
 
 /*!
    \brief Open existing signature file
 
-   \param group group name (may be fully qualified)
-   \param subgroup subgroup name in given group
+   Use fully qualified names for signatures from other mapsets
+
    \param name signature filename
 
-   \return pointer to FILE*
+   \return pointer to FILE
    \return NULL on error
-*/
-FILE *I_fopen_signature_file_old(const char *group,
-				 const char *subgroup, const char *name)
+ */
+FILE *I_fopen_signature_file_old(const char *name)
 {
-    char element[GPATH_MAX];
-    char group_name[GNAME_MAX], group_mapset[GMAPSET_MAX];
+    char sig_name[GNAME_MAX], sig_mapset[GMAPSET_MAX];
+    char element[GNAME_MAX];
     FILE *fd;
 
-    G_unqualified_name(group, NULL, group_name, group_mapset);
-    sprintf(element, "subgroup/%s/sig/%s", subgroup, name);
+    if (G_unqualified_name(name, NULL, sig_name, sig_mapset) == 0)
+        strcpy(sig_mapset, G_mapset());
+
+    I__get_signatures_element(element, I_SIGFILE_TYPE_SIG);
+    fd = G_fopen_old(element, sig_name, sig_mapset);
 
-    fd = G_fopen_old_misc("group", element, group_name, group_mapset);
-    
     return fd;
 }

+ 268 - 20
lib/imagery/sigset.c

@@ -2,9 +2,10 @@
 #include <stdlib.h>
 #include <grass/imagery.h>
 #include <grass/gis.h>
+#include <grass/glocale.h>
 
 static int gettag(FILE *, char *);
-static int get_nbands(FILE *, struct SigSet *);
+static int get_bandrefs(FILE *, struct SigSet *);
 static int get_title(FILE *, struct SigSet *);
 static int get_class(FILE *, struct SigSet *);
 static int get_classnum(FILE *, struct ClassSig *);
@@ -53,9 +54,20 @@ struct ClassData *I_AllocClassData(struct SigSet *S,
     return Data;
 }
 
-int I_InitSigSet(struct SigSet *S)
+/*!
+ * \brief Initialize struct SigSet before use
+ *
+ * No need to call before calling I_ReadSigSet.
+ *
+ * \param *Signature to initialize
+ * \param nbands band (imagery group member) count
+ */
+int I_InitSigSet(struct SigSet *S, int nbands)
 {
-    S->nbands = 0;
+    S->nbands = nbands;
+    S->bandrefs = (char **)G_malloc(nbands * sizeof(char **));
+    for (int i = 0; i < nbands; i++)
+        S->bandrefs[i] = NULL;
     S->nclasses = 0;
     S->ClassSig = NULL;
     S->title = NULL;
@@ -63,13 +75,6 @@ int I_InitSigSet(struct SigSet *S)
     return 0;
 }
 
-int I_SigSetNBands(struct SigSet *S, int nbands)
-{
-    S->nbands = nbands;
-
-    return 0;
-}
-
 struct ClassSig *I_NewClassSig(struct SigSet *S)
 {
     struct ClassSig *Sp;
@@ -121,20 +126,44 @@ struct SubSig *I_NewSubSig(struct SigSet *S, struct ClassSig *C)
 
 #define eq(a,b) strcmp(a,b)==0
 
+/*!
+ * \brief Read sigset signatures from file
+ *
+ * File stream should be opened in advance by call to
+ * I_fopen_sigset_file_old()
+ * It is up to caller to fclose the file stream afterwards.
+ *
+ * There is no need to initialise struct SigSet in advance, as this
+ * function internally calls I_InitSigSet.
+ *
+ * \param pointer to FILE*
+ * \param pointer to struct SigSet *S
+ *
+ * \return 1 on success, -1 on failure
+ */
 int I_ReadSigSet(FILE * fd, struct SigSet *S)
 {
     char tag[256];
+    unsigned int version;
 
-    I_InitSigSet(S);
+    if (fscanf(fd, "%u", &version) != 1) {
+        G_warning(_("Invalid signature file"));
+        return -1;
+    }
+    if (version != 1) {
+        G_warning(_("Invalid signature file version"));
+        return -1;
+    }
 
+    I_InitSigSet(S, 0);
     while (gettag(fd, tag)) {
 	if (eq(tag, "title:"))
 	    if (get_title(fd, S) != 0)
             return -1;
-	if (eq(tag, "nbands:"))
-	    if (get_nbands(fd, S) != 0)
+	if (eq(tag, "bandrefs:"))
+        if (get_bandrefs(fd, S) != 0)
             return -1;
-	if (eq(tag, "class:"))
+    if (eq(tag, "class:"))
 	    if (get_class(fd, S) != 0)
             return -1;
     }
@@ -143,16 +172,38 @@ int I_ReadSigSet(FILE * fd, struct SigSet *S)
 
 static int gettag(FILE * fd, char *tag)
 {
-    if (fscanf(fd, "%s", tag) != 1)
+    if (fscanf(fd, "%255s", tag) != 1)
 	return 0;
     G_strip(tag);
     return 1;
 }
 
-static int get_nbands(FILE * fd, struct SigSet *S)
+static int get_bandrefs(FILE * fd, struct SigSet *S)
 {
-    if (fscanf(fd, "%d", &S->nbands) != 1)
+    char **bandrefs;
+    char *bandrefs_str;
+
+    if (fscanf(fd, "%m[^\n]", &bandrefs_str) != 1) {
+        G_warning(_("Error reading band references from sigset file"));
         return -1;
+    }
+
+    G_strip(bandrefs_str);
+    bandrefs = G_tokenize(bandrefs_str, " ");
+    S->nbands = G_number_of_tokens(bandrefs);
+    if (!(S->nbands > 0)) {
+        G_warning(_("Signature file does not contain bands"));
+        return -1;
+    }
+    S->bandrefs = (char **)G_realloc(S->bandrefs, S->nbands * sizeof(char **));
+    for (unsigned int i = S->nbands; i--;) {
+        if (strlen(bandrefs[i]) > (GNAME_MAX - 1)) {
+            G_warning(_("Invalid sigset file: band reference length limit exceeded"));
+            return -1;
+        }
+        S->bandrefs[i] = (char *)G_malloc(GNAME_MAX * sizeof(char *));
+        strcpy(S->bandrefs[i], bandrefs[i]);
+    }
 
     return 0;
 }
@@ -162,8 +213,9 @@ static int get_title(FILE * fd, struct SigSet *S)
     char title[1024];
 
     *title = 0;
-    if (fscanf(fd, "%[^\n]", title) != 1)
+    if (fscanf(fd, "%1024[^\n]", title) != 1)
         return -1;
+    G_strip(title);
     I_SetSigTitle(S, title);
 
     return 0;
@@ -216,8 +268,9 @@ static int get_classtitle(FILE * fd, struct ClassSig *C)
     char title[1024];
 
     *title = 0;
-    if (fscanf(fd, "%[^\n]", title) != 1)
+    if (fscanf(fd, "%1024[^\n]", title) != 1)
         return -1;
+    G_strip(title);
     I_SetClassTitle(C, title);
 
     return 0;
@@ -322,8 +375,14 @@ int I_WriteSigSet(FILE * fd, const struct SigSet *S)
     const struct SubSig *Sp;
     int i, j, b1, b2;
 
+    /* This is version 1 sigset file format */
+    fprintf(fd, "1\n");
     fprintf(fd, "title: %s\n", I_GetSigTitle(S));
-    fprintf(fd, "nbands: %d\n", S->nbands);
+    fprintf(fd, "bandrefs: ");
+    for (i = 0; i < S->nbands; i++) {
+        fprintf(fd, "%s ", S->bandrefs[i]);
+    }
+    fprintf(fd, "\n");
     for (i = 0; i < S->nclasses; i++) {
 	Cp = &S->ClassSig[i];
 	if (!Cp->used)
@@ -357,3 +416,192 @@ int I_WriteSigSet(FILE * fd, const struct SigSet *S)
 
     return 0;
 }
+
+/*!
+ * \brief Reorder struct SigSet to match imagery group member order
+ *
+ * The function will check for band reference match between sigset struct
+ * and imagery group.
+ *
+ * In the case of a complete band reference match, values of passed in
+ * struct SigSet are reordered to match the order of imagery group items.
+ * This reordering is done only for items present in the sigset file.
+ * Thus reordering should be done only after calling I_ReadSigSet.
+ *
+ * If all band references are not identical (in
+ * arbitrary order), function will return two dimensional array with
+ * comma separated list of:
+ *      - [0] band references present in the signature struct but
+ * absent in the imagery group
+ *      - [1] band references present in the imagery group but
+ * absent in the signature struct
+ *
+ * If no mismatch of band references for signatures or imagery group are
+ * detected (== all are present in the other list), a NULL value will be
+ * returned in the particular list of mismatches (not an empty string).
+ * For example:
+ * \code if (ret && ret[1]) printf("List of imagery group bands without signatures: %s\n, ret[1]); \endcode
+ *
+ * \param *SigSet existing signatures to check & sort
+ * \param *Ref group reference
+ *
+ * \return NULL successfully sorted
+ * \return err_array two comma separated lists of mismatches
+ */
+char **I_SortSigSetByBandref(struct SigSet *S, const struct Ref *R) {
+    unsigned int total, complete;
+    unsigned int *match1, *match2, mc1, mc2, *new_order;
+    double ***new_means, ****new_vars;
+    char **group_bandrefs, **mismatches, **new_bandrefs;
+
+    /* Safety measure. Untranslated as this should not happen in production! */
+    if (S->nbands < 1 || R->nfiles < 1)
+        G_fatal_error("Programming error. Invalid length structs passed to "
+                      "I_sort_signatures_by_bandref(%d, %d);", S->nbands,  R->nfiles);
+
+    /* Obtain group band references */
+    group_bandrefs = (char **)G_malloc(R->nfiles * sizeof(char *));
+    for (unsigned int j = R->nfiles; j--;) {
+        group_bandrefs[j] = Rast_read_bandref(R->file[j].name, R->file[j].mapset);
+    }
+
+    /* If lengths are not equal, there will be a mismatch */
+    complete = S->nbands == R->nfiles;
+
+    /* Initialize match tracker */
+    new_order = (unsigned int *)G_malloc(S->nbands * sizeof(unsigned int));
+    match1 = (unsigned int *)G_calloc(S->nbands, sizeof(unsigned int));
+    match2 = (unsigned int *)G_calloc(R->nfiles, sizeof(unsigned int));
+
+    /* Allocate memory for temporary storage of sorted values */
+    new_bandrefs = (char **)G_malloc(S->nbands * sizeof(char *));
+    new_means = (double ***)G_malloc(S->nclasses * sizeof(double **));
+    // new_vars[S.ClassSig[x]][.SubSig[y]][R[band1]][R[band1]]
+    new_vars = (double ****)G_malloc(S->nclasses * sizeof(double ***));
+    for (unsigned int c = S->nclasses; c--;) {
+        new_means[c] = (double **)G_malloc(S->ClassSig[c].nsubclasses * sizeof(double *));
+        new_vars[c] = (double ***)G_malloc(S->ClassSig[c].nsubclasses * sizeof(double **));
+        for (unsigned int s = S->ClassSig[c].nsubclasses; s--;) {
+            new_means[c][s] = (double *)G_malloc(S->nbands * sizeof(double));
+            new_vars[c][s] = (double **)G_malloc(S->nbands * sizeof(double *));
+            for (unsigned int i = S->nbands; i--;)
+                new_vars[c][s][i] = (double *)G_malloc(S->nbands * sizeof(double));
+        }
+    }
+
+    /* Obtain order of matching items */
+    for (unsigned int j = R->nfiles; j--;) {
+        for (unsigned int i = S->nbands; i--;) {
+            if (S->bandrefs[i] && group_bandrefs[j] &&
+                !strcmp(S->bandrefs[i], group_bandrefs[j])) {
+                    if (complete) {
+                        /* Reorder pointers to existing strings only */
+                        new_bandrefs[j] = S->bandrefs[i];
+                        new_order[i] = j;
+                    }
+                    /* Keep a track of matching items for error reporting */
+                    match1[i] = 1;
+                    match2[j] = 1;
+                    break;
+            }
+        }
+    }
+
+    /* Check for band reference mismatch */
+    mc1 = mc2 = 0;
+    mismatches = (char **)G_malloc(2 * sizeof(char **));
+    mismatches[0] = NULL;
+    mismatches[1] = NULL;
+    total = 1;
+    for (unsigned int i = 0; i < S->nbands; i++) {
+        if (!match1[i]) {
+            if (S->bandrefs[i])
+                total = total + strlen(S->bandrefs[i]);
+            else
+                total = total + 24;
+            mismatches[0] = (char *)G_realloc(mismatches[0], total * sizeof(char *));
+            if (mc1)
+                strcat(mismatches[0], ",");
+            else
+                mismatches[0][0] = '\0';
+            if (S->bandrefs[i])
+                strcat(mismatches[0], S->bandrefs[i]);
+            else
+                strcat(mismatches[0], "<band reference missing>");
+            mc1++;
+            total = total + 1;
+        }
+    }
+    total = 1;
+    for (unsigned int j = 0; j < R->nfiles; j++) {
+        if (!match2[j]) {
+            if (group_bandrefs[j])
+                total = total + strlen(group_bandrefs[j]);
+            else
+                total = total + 24;
+            mismatches[1] = (char *)G_realloc(mismatches[1], total * sizeof(char *));
+            if (mc2)
+                strcat(mismatches[1], ",");
+            else
+                mismatches[1][0] = '\0';
+            if (group_bandrefs[j])
+                strcat(mismatches[1], group_bandrefs[j]);
+            else
+                strcat(mismatches[1], "<band reference missing>");
+            mc2++;
+            total = total + 1;
+        }
+    }
+
+    /* Swap mean and var matrix values in each of classes */
+    if (!mc1 && !mc2) {
+        for (unsigned int c = S->nclasses; c--;) {
+            for (unsigned int s = S->ClassSig[c].nsubclasses; s--;) {
+                for (unsigned int b1 = 0; b1 < S->nbands; b1++) {
+                    new_means[c][s][new_order[b1]] = S->ClassSig[c].SubSig[s].means[b1];
+                    for (unsigned int b2 = 0; b2 < S->nbands; b2++) {
+                        new_vars[c][s][new_order[b1]][new_order[b2]] = S->ClassSig[c].SubSig[s].R[b1][b2];
+                    }
+                }
+            }
+        }
+
+        /* Replace values in struct with ordered ones */
+        memcpy(S->bandrefs, new_bandrefs, S->nbands * sizeof(char **));
+        for (unsigned int c = S->nclasses; c--;) {
+            for (unsigned int s = S->ClassSig[c].nsubclasses; s--;) {
+                memcpy(S->ClassSig[c].SubSig[s].means, new_means[c][s], S->nbands * sizeof(double));
+                for (unsigned int i = S->nbands; i--;)
+                    memcpy(S->ClassSig[c].SubSig[s].R[i], new_vars[c][s][i], S->nbands * sizeof(double));
+            }
+        }
+    }
+
+    /* Clean up */
+    for (unsigned int j = R->nfiles; j--;)
+        free(group_bandrefs[j]);
+    free(group_bandrefs);
+    free(new_order);
+    free(match1);
+    free(match2);
+    free(new_bandrefs);
+    for (unsigned int c = S->nclasses; c--;) {
+        for (unsigned int s = S->ClassSig[c].nsubclasses; s--;) {
+            free(new_means[c][s]);
+            for (unsigned int i = S->nbands; i--;)
+                free(new_vars[c][s][i]);
+            free(new_vars[c][s]);
+        }
+        free(new_means[c]);
+        free(new_vars[c]);
+    }
+
+    free(new_means);
+    free(new_vars);
+
+    if (mc1 || mc2) {
+        return mismatches;
+    }
+    free(mismatches);
+    return NULL;
+}

+ 37 - 61
lib/imagery/sigsetfile.c

@@ -1,15 +1,15 @@
 /*!
-  \file lib/imagery/sigsetfile.c
- 
-  \brief Imagery Library - Signature file functions (statistics for i.smap)
- 
-  (C) 2001-2011, 2013 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 USA CERL
-*/
+   \file lib/imagery/sigsetfile.c
+
+   \brief Imagery Library - Signature file functions (statistics for i.smap)
+
+   (C) 2001-2011, 2013, 2021 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 USA CERL
+ */
 
 #include <string.h>
 
@@ -18,70 +18,46 @@
 #include <grass/glocale.h>
 
 /*!
-  \brief Create new signiture file in given group/subgroup
+   \brief Create new sigset file
 
-  Note: Prints warning on error and returns NULL.
+   \param name name of sigset file
 
-  \param group name of group
-  \param subgroup name of subgroup
-  \param name name of signiture file
-
-  \return pointer to FILE
-  \return NULL on error
-*/
-FILE *I_fopen_sigset_file_new(const char *group, const char *subgroup,
-			      const char *name)
+   \return pointer to FILE
+   \return NULL on error
+ */
+FILE *I_fopen_sigset_file_new(const char *name)
 {
-    char element[GPATH_MAX];
-    char group_name[GNAME_MAX], mapset[GMAPSET_MAX];
+    char element[GNAME_MAX];
     FILE *fd;
 
-    if (G_name_is_fully_qualified(group, group_name, mapset)) {
-	if (strcmp(mapset, G_mapset()) != 0)
-	    G_warning(_("Unable to create signature file <%s> for subgroup <%s> "
-			"of group <%s> - <%s> is not current mapset"),
-		      name, subgroup, group, mapset);
-    }
-    else { 
-	strcpy(group_name, group);
-    }
-
-    /* create sigset directory */
-    sprintf(element, "%s/subgroup/%s/sigset", group_name, subgroup);
-    G__make_mapset_element_misc("group", element);
-
-    sprintf(element, "subgroup/%s/sigset/%s", subgroup, name);
-
-    fd = G_fopen_new_misc("group", element, group_name);
-    if (fd == NULL)
-	G_warning(_("Unable to create signature file <%s> for subgroup <%s> "
-		    "of group <%s>"),
-		  name, subgroup, group);
-    
+    /* create sig directory */
+    I__make_signatures_element(I_SIGFILE_TYPE_SIGSET);
+
+    I__get_signatures_element(element, I_SIGFILE_TYPE_SIGSET);
+    fd = G_fopen_new(element, name);
+
     return fd;
 }
 
 /*!
-  \brief Open existing signiture file
+   \brief Open existing sigset signature file
 
-  \param group name of group (may be fully qualified)
-  \param subgroup name of subgroup
-  \param name name of signiture file
+   \param name name of signature file (may be fully qualified)
 
-  \return pointer to FILE*
-  \return NULL on error
-*/
-FILE *I_fopen_sigset_file_old(const char *group, const char *subgroup,
-			      const char *name)
+   \return pointer to FILE*
+   \return NULL on error
+ */
+FILE *I_fopen_sigset_file_old(const char *name)
 {
-    char element[GPATH_MAX];
-    char group_name[GNAME_MAX], group_mapset[GMAPSET_MAX];
+    char sig_name[GNAME_MAX], sig_mapset[GMAPSET_MAX];
+    char element[GNAME_MAX];
     FILE *fd;
 
-    G_unqualified_name(group, NULL, group_name, group_mapset);
-    sprintf(element, "subgroup/%s/sigset/%s", subgroup, name);
+    if (G_unqualified_name(name, NULL, sig_name, sig_mapset) == 0)
+        strcpy(sig_mapset, G_mapset());
+
+    I__get_signatures_element(element, I_SIGFILE_TYPE_SIGSET);
+    fd = G_fopen_old(element, sig_name, sig_mapset);
 
-    fd = G_fopen_old_misc("group", element, group_name, group_mapset);
-    
     return fd;
 }

+ 151 - 0
lib/imagery/testsuite/test_imagery_find.py

@@ -0,0 +1,151 @@
+"""Test of imagery library file searching 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 os
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+from grass.script.core import tempname
+from grass.pygrass import utils
+from grass.pygrass.gis import Mapset
+
+from grass.lib.gis import G_mapset_path
+from grass.lib.imagery import (
+    I_SIGFILE_TYPE_SIG,
+    I_SIGFILE_TYPE_SIGSET,
+    I_find_signature,
+    I_find_signature2,
+)
+
+
+class FindSignatureTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.mpath = utils.decode(G_mapset_path())
+        cls.mapset_name = Mapset().name
+        # As signatures are created directly not via signature creation
+        # tools, we must ensure signature directories exist
+        os.makedirs(f"{cls.mpath}/signatures/sig/", exist_ok=True)
+        os.makedirs(f"{cls.mpath}/signatures/sigset/", exist_ok=True)
+        cls.sig_name1 = tempname(10)
+        cls.sigfile_name1 = f"{cls.mpath}/signatures/sigset/{cls.sig_name1}"
+        open(cls.sigfile_name1, "a").close()
+        cls.sig_name2 = tempname(10)
+        cls.sigfile_name2 = f"{cls.mpath}/signatures/sig/{cls.sig_name2}"
+        open(cls.sigfile_name2, "a").close()
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            os.remove(cls.sigfile_name1)
+            os.remove(cls.sigfile_name2)
+        except OSError:
+            pass
+
+    def test_find_sig(self):
+        # Non existing without a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, tempname(10), None)
+        self.assertFalse(ret)
+        # Non existing with a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, tempname(10), self.mapset_name)
+        self.assertFalse(ret)
+        # Sigset with sig type should equal non existing
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, self.sig_name1, self.mapset_name)
+        self.assertFalse(ret)
+        # Existing without a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, self.sig_name2, None)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing with a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, self.sig_name2, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing in a different mapset should fail
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, self.sig_name2, "PERMANENT")
+        self.assertFalse(ret)
+
+    def test_find_sigset(self):
+        # Non existing without a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, tempname(10), None)
+        self.assertFalse(ret)
+        # Non existing with a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, tempname(10), self.mapset_name)
+        self.assertFalse(ret)
+        # Sig with sigset type should equal non existing
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, self.sig_name2, self.mapset_name)
+        self.assertFalse(ret)
+        # Existing without a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, self.sig_name1, None)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing with a mapset
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, self.sig_name1, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing in a different mapset should fail
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, self.sig_name1, "PERMANENT")
+        self.assertFalse(ret)
+
+    def test_find2_sig(self):
+        # Non existing without a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIG, tempname(10), None)
+        self.assertFalse(ret)
+        # Non existing with a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIG, tempname(10), self.mapset_name)
+        self.assertFalse(ret)
+        # Sigset with sig type should equal non existing
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIG, self.sig_name1, self.mapset_name)
+        self.assertFalse(ret)
+        # Existing without a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIG, self.sig_name2, None)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing with a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIG, self.sig_name2, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing in a different mapset should fail
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIG, self.sig_name2, "PERMANENT")
+        self.assertFalse(ret)
+
+    def test_find2_sigset(self):
+        # Non existing without a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIGSET, tempname(10), None)
+        self.assertFalse(ret)
+        # Non existing with a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIGSET, tempname(10), self.mapset_name)
+        self.assertFalse(ret)
+        # Sig with sigset type should equal non existing
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIGSET, self.sig_name2, self.mapset_name)
+        self.assertFalse(ret)
+        # Existing without a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIGSET, self.sig_name1, None)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing with a mapset
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIGSET, self.sig_name1, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Existing in a different mapset should fail
+        ret = I_find_signature2(I_SIGFILE_TYPE_SIGSET, self.sig_name1, "PERMANENT")
+        self.assertFalse(ret)
+
+
+if __name__ == "__main__":
+    test()

+ 685 - 0
lib/imagery/testsuite/test_imagery_sigfile.py

@@ -0,0 +1,685 @@
+"""Test of imagery library signature file 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 os
+import stat
+import ctypes
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+from grass.script.core import tempname
+from grass.pygrass import utils
+from grass.pygrass.gis import Mapset
+
+from grass.lib.gis import G_mapset_path
+from grass.lib.raster import Rast_write_bandref
+from grass.lib.imagery import (
+    Signature,
+    Ref,
+    I_init_signatures,
+    I_new_signature,
+    I_fopen_signature_file_new,
+    I_write_signatures,
+    I_fopen_signature_file_old,
+    I_read_signatures,
+    I_sort_signatures_by_bandref,
+    I_free_signatures,
+    I_init_group_ref,
+    I_add_file_to_group_ref,
+    I_free_group_ref,
+)
+
+
+class SignatureFileTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
+        cls.mpath = utils.decode(G_mapset_path())
+        cls.mapset_name = Mapset().name
+        cls.sig_name = tempname(10)
+        cls.sigfile_name = f"{cls.mpath}/signatures/sig/{cls.sig_name}"
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            os.remove(cls.sigfile_name)
+        except OSError:
+            pass
+
+    def test_I_fopen_signature_file_old_fail(self):
+        sigfile = I_fopen_signature_file_old(tempname(10))
+        self.assertFalse(sigfile)
+
+    def test_roundtrip_signature_v1_norgb_one_band(self):
+        """Test writing and reading back signature file (v1)
+        wiht a single band"""
+
+        # Create signature struct
+        So = Signature()
+        I_init_signatures(ctypes.byref(So), 1)
+        self.assertEqual(So.nbands, 1)
+        sig_count = I_new_signature(ctypes.byref(So))
+        self.assertEqual(sig_count, 1)
+
+        # Fill signatures struct with data
+        So.title = b"Signature title"
+        So.bandrefs[0] = ctypes.create_string_buffer(b"The_Doors")
+        So.sig[0].status = 1
+        So.sig[0].have_color = 0
+        So.sig[0].npoints = 42
+        So.sig[0].desc = b"my label"
+        So.sig[0].mean[0] = 2.5
+        So.sig[0].var[0][0] = 0.7
+
+        # Write signatures to file
+        p_new_sigfile = I_fopen_signature_file_new(self.sig_name)
+        sig_stat = os.stat(self.sigfile_name)
+        self.assertTrue(stat.S_ISREG(sig_stat.st_mode))
+        I_write_signatures(p_new_sigfile, ctypes.byref(So))
+        self.libc.fclose(p_new_sigfile)
+
+        # Read back from signatures file
+        Sn = Signature()
+        fq_name = f"{self.sig_name}@{self.mapset_name}"
+        p_old_sigfile = I_fopen_signature_file_old(fq_name)
+        ret = I_read_signatures(p_old_sigfile, ctypes.byref(Sn))
+        self.assertEqual(ret, 1)
+        self.assertEqual(Sn.title, b"Signature title")
+        self.assertEqual(Sn.nbands, 1)
+        bandref = utils.decode(ctypes.cast(Sn.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Doors")
+        self.assertEqual(Sn.sig[0].status, 1)
+        self.assertEqual(Sn.sig[0].have_color, 0)
+        self.assertEqual(Sn.sig[0].npoints, 42)
+        self.assertEqual(Sn.sig[0].desc, b"my label")
+        self.assertEqual(Sn.sig[0].mean[0], 2.5)
+        self.assertEqual(Sn.sig[0].var[0][0], 0.7)
+
+        # Free signature struct after use
+        So.bandrefs[0] = None
+        I_free_signatures(ctypes.byref(So))
+        I_free_signatures(ctypes.byref(Sn))
+        self.assertEqual(Sn.nbands, 0)
+        self.assertEqual(Sn.nsigs, 0)
+
+    def test_broken_signature_v1_norgb(self):
+        """Test reading back signature file (v1) should fail due to
+        single band reference exceeding maximum length"""
+
+        # Create signature struct
+        So = Signature()
+        I_init_signatures(ctypes.byref(So), 1)
+        self.assertEqual(So.nbands, 1)
+        sig_count = I_new_signature(ctypes.byref(So))
+        self.assertEqual(sig_count, 1)
+
+        # Fill signatures struct with data
+        So.title = b"Signature title"
+        # len(tempname(251)) == 255
+        So.bandrefs[0] = ctypes.create_string_buffer(tempname(251).encode())
+        So.sig[0].status = 1
+        So.sig[0].have_color = 0
+        So.sig[0].npoints = 42
+        So.sig[0].desc = b"my label"
+        So.sig[0].mean[0] = 2.5
+        So.sig[0].var[0][0] = 0.7
+
+        # Write signatures to file
+        p_new_sigfile = I_fopen_signature_file_new(self.sig_name)
+        sig_stat = os.stat(self.sigfile_name)
+        self.assertTrue(stat.S_ISREG(sig_stat.st_mode))
+        I_write_signatures(p_new_sigfile, ctypes.byref(So))
+        self.libc.fclose(p_new_sigfile)
+
+        # Read back from signatures file
+        Sn = Signature()
+        p_old_sigfile = I_fopen_signature_file_old(self.sig_name)
+        ret = I_read_signatures(p_old_sigfile, ctypes.byref(Sn))
+        self.assertEqual(ret, -1)
+
+        So.bandrefs[0] = None
+        I_free_signatures(ctypes.byref(So))
+        I_free_signatures(ctypes.byref(Sn))
+
+    def test_roundtrip_signature_v1_norgb_two_bands(self):
+        """Test writing and reading back signature (v1) with two bands"""
+
+        # Create signature struct
+        So = Signature()
+        I_init_signatures(ctypes.byref(So), 2)
+        self.assertEqual(So.nbands, 2)
+        sig_count = I_new_signature(ctypes.byref(So))
+        self.assertEqual(sig_count, 1)
+        sig_count = I_new_signature(ctypes.byref(So))
+        self.assertEqual(sig_count, 2)
+
+        # Fill signatures struct with data
+        So.title = b"Signature title"
+        So.bandrefs[0] = ctypes.create_string_buffer(b"The_Doors")
+        So.bandrefs[1] = ctypes.create_string_buffer(b"The_Who")
+        So.sig[0].status = 1
+        So.sig[0].have_color = 0
+        So.sig[0].npoints = 42
+        So.sig[0].desc = b"my label1"
+        So.sig[0].mean[0] = 2.5
+        So.sig[0].mean[1] = 3.5
+        So.sig[0].var[0][0] = 0.7
+        So.sig[0].var[1][0] = 0.2
+        So.sig[0].var[1][1] = 0.8
+        So.sig[1].status = 1
+        So.sig[1].have_color = 0
+        So.sig[1].npoints = 69
+        So.sig[1].desc = b"my label2"
+        So.sig[1].mean[0] = 3.5
+        So.sig[1].mean[1] = 4.5
+        So.sig[1].var[0][0] = 1.7
+        So.sig[1].var[1][0] = 1.2
+        So.sig[1].var[1][1] = 1.8
+
+        # Write signatures to file
+        p_new_sigfile = I_fopen_signature_file_new(self.sig_name)
+        sig_stat = os.stat(self.sigfile_name)
+        self.assertTrue(stat.S_ISREG(sig_stat.st_mode))
+        I_write_signatures(p_new_sigfile, ctypes.byref(So))
+        self.libc.fclose(p_new_sigfile)
+
+        # Read back from signatures file
+        Sn = Signature()
+        p_old_sigfile = I_fopen_signature_file_old(self.sig_name)
+        ret = I_read_signatures(p_old_sigfile, ctypes.byref(Sn))
+        self.assertEqual(ret, 1)
+        self.assertEqual(Sn.title, b"Signature title")
+        self.assertEqual(Sn.nbands, 2)
+        bandref = utils.decode(ctypes.cast(Sn.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Doors")
+        self.assertEqual(Sn.sig[0].status, 1)
+        self.assertEqual(Sn.sig[0].have_color, 0)
+        self.assertEqual(Sn.sig[0].npoints, 42)
+        self.assertEqual(Sn.sig[0].desc, b"my label1")
+        self.assertEqual(Sn.sig[0].mean[0], 2.5)
+        self.assertEqual(Sn.sig[0].mean[1], 3.5)
+        self.assertEqual(Sn.sig[0].var[0][0], 0.7)
+        self.assertEqual(Sn.sig[0].var[1][0], 0.2)
+        self.assertEqual(Sn.sig[0].var[1][1], 0.8)
+        bandref = utils.decode(ctypes.cast(Sn.bandrefs[1], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Who")
+        self.assertEqual(Sn.sig[1].status, 1)
+        self.assertEqual(Sn.sig[1].have_color, 0)
+        self.assertEqual(Sn.sig[1].npoints, 69)
+        self.assertEqual(Sn.sig[1].desc, b"my label2")
+        self.assertEqual(Sn.sig[1].mean[0], 3.5)
+        self.assertEqual(Sn.sig[1].mean[1], 4.5)
+        self.assertEqual(Sn.sig[1].var[0][0], 1.7)
+        self.assertEqual(Sn.sig[1].var[1][0], 1.2)
+        self.assertEqual(Sn.sig[1].var[1][1], 1.8)
+
+        # Free signature struct after use
+        So.bandrefs[0] = None
+        So.bandrefs[1] = None
+        I_free_signatures(ctypes.byref(So))
+        I_free_signatures(ctypes.byref(Sn))
+        self.assertEqual(Sn.nbands, 0)
+        self.assertEqual(Sn.nsigs, 0)
+
+
+class SortSignaturesByBandrefTest(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
+        cls.mapset = Mapset().name
+        cls.map1 = tempname(10)
+        cls.bandref1 = "The_Doors"
+        cls.map2 = tempname(10)
+        cls.bandref2 = "The_Who"
+        cls.map3 = tempname(10)
+        cls.use_temp_region()
+        cls.runModule("g.region", n=1, s=0, e=1, w=0, res=1)
+        cls.runModule("r.mapcalc", expression=f"{cls.map1} = 1")
+        cls.runModule("r.mapcalc", expression=f"{cls.map2} = 1")
+        cls.runModule("r.mapcalc", expression=f"{cls.map3} = 1")
+        Rast_write_bandref(cls.map1, cls.bandref1)
+        Rast_write_bandref(cls.map2, cls.bandref2)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.del_temp_region()
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map1)
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map2)
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map3)
+
+    def test_symmetric_complete_difference(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+
+        # Prepare signature struct
+        S = Signature()
+        I_init_signatures(ctypes.byref(S), 1)
+        self.assertEqual(S.nbands, 1)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 1)
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Troggs")
+        S.title = b"Signature title"
+        S.sig[0].status = 1
+        S.sig[0].have_color = 0
+        S.sig[0].npoints = 42
+        S.sig[0].desc = b"my label"
+        S.sig[0].mean[0] = 2.5
+        S.sig[0].var[0][0] = 0.7
+
+        # This should result in two error strings in ret
+        ret = I_sort_signatures_by_bandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertTrue(bool(ret))
+        sig_err = utils.decode(ctypes.cast(ret[0], ctypes.c_char_p).value)
+        ref_err = utils.decode(ctypes.cast(ret[1], ctypes.c_char_p).value)
+        self.assertEqual(sig_err, "The_Troggs")
+        self.assertEqual(ref_err, "The_Doors")
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        S.bandrefs[0] = None  # C should not call free() on memory allocated by python
+        I_free_signatures(ctypes.byref(S))
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_asymmetric_complete_difference(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+
+        # Prepare signature struct
+        S = Signature()
+        I_init_signatures(ctypes.byref(S), 1)
+        self.assertEqual(S.nbands, 1)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 1)
+        S.title = b"Signature title"
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Troggs")
+        S.sig[0].status = 1
+        S.sig[0].have_color = 0
+        S.sig[0].npoints = 42
+        S.sig[0].desc = b"my label"
+        S.sig[0].mean[0] = 2.5
+        S.sig[0].var[0][0] = 0.7
+
+        # This should result in two error strings in ret
+        ret = I_sort_signatures_by_bandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertTrue(bool(ret))
+        sig_err = utils.decode(ctypes.cast(ret[0], ctypes.c_char_p).value)
+        ref_err = utils.decode(ctypes.cast(ret[1], ctypes.c_char_p).value)
+        self.assertEqual(sig_err, "The_Troggs")
+        self.assertEqual(ref_err, "The_Doors,The_Who")
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        S.bandrefs[0] = None
+        I_free_signatures(ctypes.byref(S))
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_missing_bandref(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+        ret = I_add_file_to_group_ref(self.map3, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 2)
+
+        # Prepare signature struct
+        S = Signature()
+        I_init_signatures(ctypes.byref(S), 10)
+        self.assertEqual(S.nbands, 10)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 1)
+        S.title = b"Signature title"
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Who")
+        S.sig[0].status = 1
+        S.sig[0].have_color = 0
+        S.sig[0].npoints = 42
+        S.sig[0].desc = b"my label"
+        S.sig[0].mean[0] = 2.5
+        S.sig[0].var[0][0] = 0.7
+
+        # This should result in two error strings in ret
+        ret = I_sort_signatures_by_bandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertTrue(bool(ret))
+        sig_err = utils.decode(ctypes.cast(ret[0], ctypes.c_char_p).value)
+        ref_err = utils.decode(ctypes.cast(ret[1], ctypes.c_char_p).value)
+        self.assertEqual(
+            sig_err,
+            "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>",
+        )
+        self.assertEqual(ref_err, "The_Doors,<band reference missing>")
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        S.bandrefs[0] = None
+        I_free_signatures(ctypes.byref(S))
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_single_complete_match(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+
+        # Prepare signature struct
+        S = Signature()
+        I_init_signatures(ctypes.byref(S), 1)
+        self.assertEqual(S.nbands, 1)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 1)
+        S.title = b"Signature title"
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Doors")
+        S.sig[0].status = 1
+        S.sig[0].have_color = 0
+        S.sig[0].npoints = 42
+        S.sig[0].desc = b"my label"
+        S.sig[0].mean[0] = 2.5
+        S.sig[0].var[0][0] = 0.7
+
+        # This should result in returning NULL
+        ret = I_sort_signatures_by_bandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertFalse(bool(ret))
+        bandref = utils.decode(ctypes.cast(S.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Doors")
+        self.assertEqual(S.sig[0].mean[0], 2.5)
+        self.assertEqual(S.sig[0].var[0][0], 0.7)
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        S.bandrefs[0] = None
+        I_free_signatures(ctypes.byref(S))
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_double_complete_match_reorder(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+
+        # Prepare signature struct
+        S = Signature()
+        I_init_signatures(ctypes.byref(S), 2)
+        self.assertEqual(S.nbands, 2)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 1)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 2)
+        S.title = b"Signature title"
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Who")
+        S.bandrefs[1] = ctypes.create_string_buffer(b"The_Doors")
+        S.sig[0].status = 1
+        S.sig[0].have_color = 0
+        S.sig[0].npoints = 69
+        S.sig[0].desc = b"my label2"
+        S.sig[0].mean[0] = 3.3
+        S.sig[0].mean[1] = 6.6
+        S.sig[0].var[0][0] = 1.7
+        S.sig[0].var[1][0] = 1.2
+        S.sig[0].var[1][1] = 1.8
+        S.sig[1].status = 1
+        S.sig[1].have_color = 0
+        S.sig[1].npoints = 42
+        S.sig[1].desc = b"my label1"
+        S.sig[1].mean[0] = 2.2
+        S.sig[1].mean[1] = 4.4
+        S.sig[1].var[0][0] = 0.7
+        S.sig[1].var[1][0] = 0.2
+        S.sig[1].var[1][1] = 0.8
+
+        # This should result in returning NULL
+        ret = I_sort_signatures_by_bandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertFalse(bool(ret))
+        # Band references and sig items should be swapped
+        # Static items
+        self.assertEqual(S.sig[0].npoints, 69)
+        self.assertEqual(S.sig[1].npoints, 42)
+        # Reordered items
+        bandref1 = utils.decode(ctypes.cast(S.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref1, "The_Doors")
+        bandref2 = utils.decode(ctypes.cast(S.bandrefs[1], ctypes.c_char_p).value)
+        self.assertEqual(bandref2, "The_Who")
+        self.assertEqual(S.sig[0].mean[0], 6.6)
+        self.assertEqual(S.sig[0].mean[1], 3.3)
+        self.assertEqual(S.sig[0].var[0][0], 1.8)
+        self.assertEqual(S.sig[0].var[1][0], 1.2)
+        self.assertEqual(S.sig[0].var[1][1], 1.7)
+        self.assertEqual(S.sig[1].mean[0], 4.4)
+        self.assertEqual(S.sig[1].mean[1], 2.2)
+        self.assertEqual(S.sig[1].var[0][0], 0.8)
+        self.assertEqual(S.sig[1].var[1][0], 0.2)
+        self.assertEqual(S.sig[1].var[1][1], 0.7)
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        S.bandrefs[0] = None
+        S.bandrefs[1] = None
+        I_free_signatures(ctypes.byref(S))
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_double_complete_match_same_order(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+
+        # Prepare signature struct
+        S = Signature()
+        I_init_signatures(ctypes.byref(S), 2)
+        self.assertEqual(S.nbands, 2)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 1)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 2)
+        S.title = b"Signature title"
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Who")
+        S.bandrefs[1] = ctypes.create_string_buffer(b"The_Doors")
+        S.sig[0].status = 1
+        S.sig[0].have_color = 0
+        S.sig[0].npoints = 69
+        S.sig[0].desc = b"my label2"
+        S.sig[0].mean[0] = 3.5
+        S.sig[0].var[0][0] = 1.7
+        S.sig[0].var[1][0] = 1.2
+        S.sig[0].var[1][1] = 1.8
+        S.sig[1].status = 1
+        S.sig[1].have_color = 0
+        S.sig[1].npoints = 42
+        S.sig[1].desc = b"my label1"
+        S.sig[1].mean[0] = 2.5
+        S.sig[1].var[0][0] = 0.7
+        S.sig[1].var[1][0] = 0.2
+        S.sig[1].var[1][1] = 0.8
+
+        # This should result in returning NULL
+        ret = I_sort_signatures_by_bandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertFalse(bool(ret))
+        # Band references and sig items should not be swapped
+        # Static items
+        self.assertEqual(S.sig[0].npoints, 69)
+        self.assertEqual(S.sig[1].npoints, 42)
+        # Reordered items
+        bandref1 = utils.decode(ctypes.cast(S.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref1, "The_Who")
+        bandref2 = utils.decode(ctypes.cast(S.bandrefs[1], ctypes.c_char_p).value)
+        self.assertEqual(bandref2, "The_Doors")
+        self.assertEqual(S.sig[0].mean[0], 3.5)
+        self.assertEqual(S.sig[0].var[0][0], 1.7)
+        self.assertEqual(S.sig[1].mean[0], 2.5)
+        self.assertEqual(S.sig[1].var[0][0], 0.7)
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        S.bandrefs[0] = None
+        S.bandrefs[1] = None
+        I_free_signatures(ctypes.byref(S))
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_complete_match_reorder(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+
+        # Prepare signature struct
+        S = Signature()
+        I_init_signatures(ctypes.byref(S), 2)
+        self.assertEqual(S.nbands, 2)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 1)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 2)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 3)
+        sig_count = I_new_signature(ctypes.byref(S))
+        self.assertEqual(sig_count, 4)
+        S.title = b"Signature title"
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Who")
+        S.bandrefs[1] = ctypes.create_string_buffer(b"The_Doors")
+        S.sig[0].status = 1
+        S.sig[0].have_color = 0
+        S.sig[0].npoints = 69
+        S.sig[0].desc = b"my label2"
+        S.sig[0].mean[0] = 3.3
+        S.sig[0].mean[1] = 6.6
+        S.sig[0].var[0][0] = 1.7
+        S.sig[0].var[1][0] = 1.2
+        S.sig[0].var[1][1] = 1.8
+        S.sig[1].status = 1
+        S.sig[1].have_color = 0
+        S.sig[1].npoints = 42
+        S.sig[1].desc = b"my label1"
+        S.sig[1].mean[0] = 2.2
+        S.sig[1].mean[1] = 4.4
+        S.sig[1].var[0][0] = 0.7
+        S.sig[1].var[1][0] = 0.2
+        S.sig[1].var[1][1] = 0.8
+        S.sig[2].status = 1
+        S.sig[2].have_color = 0
+        S.sig[2].npoints = 12
+        S.sig[2].desc = b"my label4"
+        S.sig[2].mean[0] = 5.5
+        S.sig[2].mean[1] = 9.9
+        S.sig[2].var[0][0] = 0.9
+        S.sig[2].var[1][0] = 0.8
+        S.sig[2].var[1][1] = 0.7
+        S.sig[3].status = 1
+        S.sig[3].have_color = 0
+        S.sig[3].npoints = 21
+        S.sig[3].desc = b"my label3"
+        S.sig[3].mean[0] = 9.9
+        S.sig[3].mean[1] = 3.3
+        S.sig[3].var[0][0] = 0.8
+        S.sig[3].var[1][0] = 0.7
+        S.sig[3].var[1][1] = 0.6
+
+        # This should result in returning NULL
+        ret = I_sort_signatures_by_bandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertFalse(bool(ret))
+        # Band references and sig items should be swapped
+        # Static items
+        self.assertEqual(S.sig[0].npoints, 69)
+        self.assertEqual(S.sig[1].npoints, 42)
+        self.assertEqual(S.sig[2].npoints, 12)
+        self.assertEqual(S.sig[3].npoints, 21)
+        # Reordered items
+        bandref1 = utils.decode(ctypes.cast(S.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref1, "The_Doors")
+        bandref2 = utils.decode(ctypes.cast(S.bandrefs[1], ctypes.c_char_p).value)
+        self.assertEqual(bandref2, "The_Who")
+        self.assertEqual(S.sig[0].mean[0], 6.6)
+        self.assertEqual(S.sig[0].mean[1], 3.3)
+        self.assertEqual(S.sig[0].var[0][0], 1.8)
+        self.assertEqual(S.sig[0].var[1][0], 1.2)
+        self.assertEqual(S.sig[0].var[1][1], 1.7)
+        self.assertEqual(S.sig[1].mean[0], 4.4)
+        self.assertEqual(S.sig[1].mean[1], 2.2)
+        self.assertEqual(S.sig[1].var[0][0], 0.8)
+        self.assertEqual(S.sig[1].var[1][0], 0.2)
+        self.assertEqual(S.sig[1].var[1][1], 0.7)
+        self.assertEqual(S.sig[2].mean[0], 9.9)
+        self.assertEqual(S.sig[2].mean[1], 5.5)
+        self.assertEqual(S.sig[2].var[0][0], 0.7)
+        self.assertEqual(S.sig[2].var[1][0], 0.8)
+        self.assertEqual(S.sig[2].var[1][1], 0.9)
+        self.assertEqual(S.sig[3].mean[0], 3.3)
+        self.assertEqual(S.sig[3].mean[1], 9.9)
+        self.assertEqual(S.sig[3].var[0][0], 0.6)
+        self.assertEqual(S.sig[3].var[1][0], 0.7)
+        self.assertEqual(S.sig[3].var[1][1], 0.8)
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        S.bandrefs[0] = None
+        S.bandrefs[1] = None
+        I_free_signatures(ctypes.byref(S))
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+
+if __name__ == "__main__":
+    test()

+ 768 - 0
lib/imagery/testsuite/test_imagery_signature_management.py

@@ -0,0 +1,768 @@
+"""Test of imagery library signature management 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 os
+import shutil
+import ctypes
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+from grass.script.core import tempname
+import grass.script as grass
+from grass.pygrass import utils
+from grass.pygrass.gis import Mapset, make_mapset
+
+from grass.lib.gis import (
+    G_mapset_path,
+    G_make_mapset,
+    G_reset_mapsets,
+    GNAME_MAX,
+    HOST_DIRSEP,
+)
+from grass.lib.imagery import (
+    I_SIGFILE_TYPE_SIG,
+    I_SIGFILE_TYPE_SIGSET,
+    I_find_signature,
+    I_signatures_remove,
+    I_signatures_copy,
+    I_signatures_rename,
+    I_signatures_list_by_type,
+    I_free_signatures_list,
+    I__get_signatures_element,
+    I__make_signatures_element,
+)
+
+
+class GetSignaturesElementTestCase(TestCase):
+    def test_get_sig(self):
+        elem = ctypes.create_string_buffer(GNAME_MAX)
+        I__get_signatures_element(elem, I_SIGFILE_TYPE_SIG)
+        self.assertEqual(utils.decode(elem.value), f"signatures{HOST_DIRSEP}sig")
+
+    def test_get_sigset(self):
+        elem = ctypes.create_string_buffer(GNAME_MAX)
+        I__get_signatures_element(elem, I_SIGFILE_TYPE_SIGSET)
+        self.assertEqual(utils.decode(elem.value), f"signatures{HOST_DIRSEP}sigset")
+
+
+class MakeSignaturesElementTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.org_mapset = Mapset()
+        cls.tmp_mapset_name = tempname(10)
+        make_mapset(mapset=cls.tmp_mapset_name)
+        cls.tmp_mapset = Mapset(mapset=cls.tmp_mapset_name)
+        cls.tmp_mapset.current()
+        cls.tmp_mapset_path = cls.tmp_mapset.path()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.org_mapset.current()
+        shutil.rmtree(cls.tmp_mapset_path, ignore_errors=True)
+
+    def test_make_sig(self):
+        I__make_signatures_element(I_SIGFILE_TYPE_SIG)
+        self.assertTrue(
+            os.path.isdir(os.path.join(self.tmp_mapset_path, "signatures", "sig"))
+        )
+        # There should not be any side effects of calling function multiple times
+        I__make_signatures_element(I_SIGFILE_TYPE_SIG)
+        self.assertTrue(
+            os.path.isdir(os.path.join(self.tmp_mapset_path, "signatures", "sig"))
+        )
+
+    def test_make_sigset(self):
+        I__make_signatures_element(I_SIGFILE_TYPE_SIGSET)
+        self.assertTrue(
+            os.path.isdir(os.path.join(self.tmp_mapset_path, "signatures", "sigset"))
+        )
+        # There should not be any side effects of calling function multiple times
+        I__make_signatures_element(I_SIGFILE_TYPE_SIGSET)
+        self.assertTrue(
+            os.path.isdir(os.path.join(self.tmp_mapset_path, "signatures", "sigset"))
+        )
+
+
+class SignaturesRemoveTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.mpath = utils.decode(G_mapset_path())
+        cls.mapset_name = Mapset().name
+        cls.sigfiles = []
+        # As signatures are created directly not via signature creation
+        # tools, we must ensure signature directories exist
+        os.makedirs(f"{cls.mpath}/signatures/sig/", exist_ok=True)
+        os.makedirs(f"{cls.mpath}/signatures/sigset/", exist_ok=True)
+
+    @classmethod
+    def tearDownClass(cls):
+        for f in cls.sigfiles:
+            try:
+                os.remove(f)
+            except OSError:
+                pass
+
+    def test_remove_existing_sig(self):
+        # This test will fail if run in PERMANENT!
+        # Set up files and mark for clean-up
+        sig_name1 = tempname(10)
+        sigfile_name1 = f"{self.mpath}/signatures/sigset/{sig_name1}"
+        open(sigfile_name1, "a").close()
+        self.sigfiles.append(sigfile_name1)
+        sig_name2 = tempname(10)
+        sigfile_name2 = f"{self.mpath}/signatures/sig/{sig_name2}"
+        open(sigfile_name2, "a").close()
+        self.sigfiles.append(sigfile_name2)
+        sig_name3 = tempname(10)
+        sigfile_name3 = f"{self.mpath}/signatures/sig/{sig_name3}"
+        open(sigfile_name3, "a").close()
+        self.sigfiles.append(sigfile_name3)
+        # Try to remove with wrong type
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIGSET, sig_name2)
+        self.assertEqual(ret, 1)
+        # Try to remove with wrong mapset
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIG, f"{sig_name2}@PERMANENT")
+        self.assertEqual(ret, 1)
+        # Should be still present
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, sig_name2, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Now remove with correct type
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIG, sig_name2)
+        self.assertEqual(ret, 0)
+        # removed should be gone
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, sig_name2, self.mapset_name)
+        self.assertFalse(ret)
+        # Others should remain
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, sig_name1, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, sig_name3, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_remove_nonexisting_sig(self):
+        # Set up files and mark for clean-up
+        sig_name1 = tempname(10)
+        sigfile_name1 = f"{self.mpath}/signatures/sigset/{sig_name1}"
+        open(sigfile_name1, "a").close()
+        self.sigfiles.append(sigfile_name1)
+        sig_name2 = tempname(10)
+        # Do not create sig_name2 matching file
+        sig_name3 = tempname(10)
+        sigfile_name3 = f"{self.mpath}/signatures/sig/{sig_name3}"
+        open(sigfile_name3, "a").close()
+        self.sigfiles.append(sigfile_name3)
+        # Now remove one (should fail as file is absent)
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIG, sig_name2)
+        self.assertEqual(ret, 1)
+        # removed should be still absent
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, sig_name2, self.mapset_name)
+        self.assertFalse(ret)
+        # All others should remain
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, sig_name1, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, sig_name3, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_remove_existing_sigset(self):
+        # Set up files and mark for clean-up
+        sig_name1 = tempname(10)
+        sigfile_name1 = f"{self.mpath}/signatures/sigset/{sig_name1}"
+        open(sigfile_name1, "a").close()
+        self.sigfiles.append(sigfile_name1)
+        sig_name2 = tempname(10)
+        sigfile_name2 = f"{self.mpath}/signatures/sigset/{sig_name2}"
+        open(sigfile_name2, "a").close()
+        self.sigfiles.append(sigfile_name2)
+        sig_name3 = tempname(10)
+        sigfile_name3 = f"{self.mpath}/signatures/sig/{sig_name3}"
+        open(sigfile_name3, "a").close()
+        self.sigfiles.append(sigfile_name3)
+        # Try to remove with wrong type
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIG, sig_name2)
+        self.assertEqual(ret, 1)
+        # Try to remove with wrong mapset
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIGSET, f"{sig_name2}@PERMANENT")
+        self.assertEqual(ret, 1)
+        # Should be still present
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, sig_name2, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        # Now remove with correct type
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIGSET, sig_name2)
+        self.assertEqual(ret, 0)
+        # removed should be gone
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, sig_name2, self.mapset_name)
+        self.assertFalse(ret)
+        # Others should remain
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, sig_name1, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, sig_name3, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_remove_nonexisting_sigset(self):
+        # Set up files and mark for clean-up
+        sig_name1 = tempname(10)
+        sigfile_name1 = f"{self.mpath}/signatures/sigset/{sig_name1}"
+        open(sigfile_name1, "a").close()
+        self.sigfiles.append(sigfile_name1)
+        sig_name2 = tempname(10)
+        # Do not create sig_name2 matching file
+        sig_name3 = tempname(10)
+        sigfile_name3 = f"{self.mpath}/signatures/sig/{sig_name3}"
+        open(sigfile_name3, "a").close()
+        self.sigfiles.append(sigfile_name3)
+        # Now remove one (should fail as file doesn't exist)
+        ret = I_signatures_remove(I_SIGFILE_TYPE_SIGSET, sig_name2)
+        self.assertEqual(ret, 1)
+        # removed should be still absent
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, sig_name2, self.mapset_name)
+        self.assertFalse(ret)
+        # All others should remain
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, sig_name1, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, sig_name3, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+
+class SignaturesCopyTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.mpath = utils.decode(G_mapset_path())
+        cls.mapset_name = Mapset().name
+        cls.sigfiles = []
+        # As signatures are created directly not via signature creation
+        # tools, we must ensure signature directories exist
+        os.makedirs(f"{cls.mpath}/signatures/sig/", exist_ok=True)
+        os.makedirs(f"{cls.mpath}/signatures/sigset/", exist_ok=True)
+        # A mapset with a random name
+        cls.src_mapset_name = tempname(10)
+        G_make_mapset(None, None, cls.src_mapset_name)
+        cls.src_mapset_path = (
+            cls.mpath.rsplit("/", maxsplit=1)[0] + "/" + cls.src_mapset_name
+        )
+        os.makedirs(f"{cls.src_mapset_path}/signatures/sig/")
+        cls.src_sig = tempname(10)
+        cls.sigfiles.append(f"{cls.src_mapset_path}/signatures/sig/{cls.src_sig}")
+        f = open(cls.sigfiles[0], "w")
+        f.write("A sig file")
+        f.close()
+        os.makedirs(f"{cls.src_mapset_path}/signatures/sigset/")
+        cls.src_sigset = tempname(10)
+        cls.sigfiles.append(f"{cls.src_mapset_path}/signatures/sigset/{cls.src_sigset}")
+        f = open(cls.sigfiles[1], "w")
+        f.write("A sigset file")
+        f.close()
+
+    @classmethod
+    def tearDownClass(cls):
+        # Remove random mapset created during setup
+        shutil.rmtree(cls.src_mapset_path, ignore_errors=True)
+        for f in cls.sigfiles:
+            try:
+                os.remove(f)
+            except OSError:
+                pass
+
+    def test_copy_to_wrong_mapset(self):
+        rnd_name = "{0}@{0}".format(tempname(10))
+        ret = I_signatures_copy(
+            I_SIGFILE_TYPE_SIG, tempname(10), self.mapset_name, rnd_name
+        )
+        self.assertEqual(ret, 1)
+
+    def test_sig_does_not_exist(self):
+        ret = I_signatures_copy(
+            I_SIGFILE_TYPE_SIG, tempname(10), self.mapset_name, tempname(10)
+        )
+        self.assertEqual(ret, 1)
+
+    def test_sigset_does_not_exist(self):
+        ret = I_signatures_copy(
+            I_SIGFILE_TYPE_SIGSET, tempname(10), self.mapset_name, tempname(10)
+        )
+        self.assertEqual(ret, 1)
+
+    def test_success_unqualified_sig(self):
+        dst = tempname(10)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, self.src_sig, self.src_mapset_name)
+        self.assertTrue(ret)
+        ret = I_signatures_copy(
+            I_SIGFILE_TYPE_SIG, self.src_sig, self.src_mapset_name, dst
+        )
+        self.sigfiles.append(f"{self.mpath}/signatures/sig/{dst}")
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_success_fq_sig(self):
+        dst = tempname(10)
+        self.sigfiles.append(f"{self.mpath}/signatures/sig/{dst}")
+        dst = dst + "@" + self.mapset_name
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, self.src_sig, self.src_mapset_name)
+        self.assertTrue(ret)
+        ret = I_signatures_copy(
+            I_SIGFILE_TYPE_SIG,
+            self.src_sig + "@" + self.src_mapset_name,
+            self.src_mapset_name,
+            dst,
+        )
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_success_unqualified_sigset(self):
+        dst = tempname(10)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(
+            I_SIGFILE_TYPE_SIGSET, self.src_sigset, self.src_mapset_name
+        )
+        self.assertTrue(ret)
+        ret = I_signatures_copy(
+            I_SIGFILE_TYPE_SIGSET, self.src_sigset, self.src_mapset_name, dst
+        )
+        self.sigfiles.append(f"{self.mpath}/signatures/sigset/{dst}")
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_success_fq_sigset(self):
+        dst = tempname(10)
+        self.sigfiles.append(f"{self.mpath}/signatures/sigset/{dst}")
+        dst = dst + "@" + self.mapset_name
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(
+            I_SIGFILE_TYPE_SIGSET, self.src_sigset, self.src_mapset_name
+        )
+        self.assertTrue(ret)
+        ret = I_signatures_copy(
+            I_SIGFILE_TYPE_SIGSET,
+            self.src_sigset + "@" + self.src_mapset_name,
+            self.src_mapset_name,
+            dst,
+        )
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+
+class SignaturesRenameTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.mpath = utils.decode(G_mapset_path())
+        cls.mapset_name = Mapset().name
+        cls.sigfiles = []
+        # As signatures are created directly not via signature creation
+        # tools, we must ensure signature directories exist
+        os.makedirs(f"{cls.mpath}/signatures/sig/", exist_ok=True)
+        os.makedirs(f"{cls.mpath}/signatures/sigset/", exist_ok=True)
+
+    @classmethod
+    def tearDownClass(cls):
+        for f in cls.sigfiles:
+            try:
+                os.remove(f)
+            except OSError:
+                pass
+
+    def test_rename_from_wrong_mapset(self):
+        rnd_name = "{0}@{0}".format(tempname(10))
+        ret = I_signatures_rename(I_SIGFILE_TYPE_SIG, rnd_name, tempname(10))
+        self.assertEqual(ret, 1)
+
+    def test_rename_to_wrong_mapset(self):
+        rnd_name = "{0}@{0}".format(tempname(10))
+        ret = I_signatures_rename(I_SIGFILE_TYPE_SIG, tempname(10), rnd_name)
+        self.assertEqual(ret, 1)
+
+    def test_sig_does_not_exist(self):
+        ret = I_signatures_rename(I_SIGFILE_TYPE_SIG, tempname(10), tempname(10))
+        self.assertEqual(ret, 1)
+
+    def test_sigset_does_not_exist(self):
+        ret = I_signatures_rename(I_SIGFILE_TYPE_SIGSET, tempname(10), tempname(10))
+        self.assertEqual(ret, 1)
+
+    def test_success_unqualified_sig(self):
+        src_sig = tempname(10)
+        sig_file = f"{self.mpath}/signatures/sig/{src_sig}"
+        self.sigfiles.append(sig_file)
+        f = open(sig_file, "w")
+        f.write("A sig file")
+        f.close()
+        dst = tempname(10)
+        self.sigfiles.append(f"{self.mpath}/signatures/sig/{dst}")
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, src_sig, self.mapset_name)
+        self.assertTrue(ret)
+        ret = I_signatures_rename(I_SIGFILE_TYPE_SIG, src_sig, dst)
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_success_fq_sig(self):
+        src_sig = tempname(10)
+        sig_file = f"{self.mpath}/signatures/sig/{src_sig}"
+        self.sigfiles.append(sig_file)
+        f = open(sig_file, "w")
+        f.write("A sig file")
+        f.close()
+        dst = tempname(10)
+        self.sigfiles.append(f"{self.mpath}/signatures/sig/{dst}")
+        dst = dst + "@" + self.mapset_name
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, src_sig, self.mapset_name)
+        self.assertTrue(ret)
+        ret = I_signatures_rename(
+            I_SIGFILE_TYPE_SIG,
+            src_sig + "@" + self.mapset_name,
+            dst,
+        )
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIG, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_success_unqualified_sigset(self):
+        src_sigset = tempname(10)
+        sigset_file = f"{self.mpath}/signatures/sigset/{src_sigset}"
+        self.sigfiles.append(sigset_file)
+        f = open(sigset_file, "w")
+        f.write("A sigset file")
+        f.close()
+        dst = tempname(10)
+        self.sigfiles.append(f"{self.mpath}/signatures/sigset/{dst}")
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, src_sigset, self.mapset_name)
+        self.assertTrue(ret)
+        ret = I_signatures_rename(I_SIGFILE_TYPE_SIGSET, src_sigset, dst)
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+    def test_success_fq_sigset(self):
+        src_sigset = tempname(10)
+        sigset_file = f"{self.mpath}/signatures/sigset/{src_sigset}"
+        self.sigfiles.append(sigset_file)
+        f = open(sigset_file, "w")
+        f.write("A sigset file")
+        f.close()
+        dst = tempname(10)
+        self.sigfiles.append(f"{self.mpath}/signatures/sigset/{dst}")
+        dst = dst + "@" + self.mapset_name
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertFalse(ret)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, src_sigset, self.mapset_name)
+        self.assertTrue(ret)
+        ret = I_signatures_rename(
+            I_SIGFILE_TYPE_SIGSET,
+            src_sigset + "@" + self.mapset_name,
+            dst,
+        )
+        self.assertEqual(ret, 0)
+        ret = I_find_signature(I_SIGFILE_TYPE_SIGSET, dst, self.mapset_name)
+        self.assertTrue(ret)
+        ms = utils.decode(ret)
+        self.assertEqual(ms, self.mapset_name)
+
+
+class SignaturesListByTypeTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.list_ptr = ctypes.POINTER(ctypes.c_char_p)
+        cls.mpath = utils.decode(G_mapset_path())
+        cls.mapset_name = Mapset().name
+        cls.sigfiles = []
+        # As signatures are created directly not via signature creation
+        # tools, we must ensure signature directories exist
+        os.makedirs(f"{cls.mpath}/signatures/sig/", exist_ok=True)
+        os.makedirs(f"{cls.mpath}/signatures/sigset/", exist_ok=True)
+        # A mapset with a random name
+        cls.rnd_mapset_name = tempname(10)
+        G_make_mapset(None, None, cls.rnd_mapset_name)
+        cls.rnd_mapset_path = (
+            cls.mpath.rsplit("/", maxsplit=1)[0] + "/" + cls.rnd_mapset_name
+        )
+        os.makedirs(f"{cls.rnd_mapset_path}/signatures/sig/")
+        os.makedirs(f"{cls.rnd_mapset_path}/signatures/sigset/")
+
+    @classmethod
+    def tearDownClass(cls):
+        # Remove random mapset created during setup
+        shutil.rmtree(cls.rnd_mapset_path, ignore_errors=True)
+        for f in cls.sigfiles:
+            try:
+                os.remove(f)
+            except OSError:
+                pass
+
+    def test_no_sigs_at_all(self):
+        # There should be no signatures in the mapset with random
+        # name and thus function call should return 0 sized list
+        sig_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIG, self.rnd_mapset_name, ctypes.byref(sig_list)
+        )
+        self.assertEqual(ret, 0)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+
+    def test_sig_in_different_mapset(self):
+        # Should return 0 signatures from a different mapset
+        local_sig = tempname(10)
+        sig_file = f"{self.mpath}/signatures/sig/{local_sig}"
+        self.sigfiles.append(sig_file)
+        f = open(sig_file, "w")
+        f.write("A sig file")
+        f.close()
+        sig_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIG, self.rnd_mapset_name, ctypes.byref(sig_list)
+        )
+        os.remove(sig_file)
+        self.assertEqual(ret, 0)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+        local_sigset = tempname(10)
+        sigset_file = f"{self.mpath}/signatures/sigset/{local_sigset}"
+        self.sigfiles.append(sigset_file)
+        f = open(sigset_file, "w")
+        f.write("A sigset file")
+        f.close()
+        sig_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIGSET, self.rnd_mapset_name, ctypes.byref(sig_list)
+        )
+        os.remove(sigset_file)
+        self.assertEqual(ret, 0)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+
+    def test_single_sig(self):
+        # Case when only a single signature file is present
+        rnd_sig = tempname(10)
+        sig_file = f"{self.rnd_mapset_path}/signatures/sig/{rnd_sig}"
+        f = open(sig_file, "w")
+        f.write("A sig file")
+        f.close()
+        sig_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIG, self.rnd_mapset_name, ctypes.byref(sig_list)
+        )
+        os.remove(sig_file)
+        self.assertEqual(ret, 1)
+        val = utils.decode(sig_list[0])
+        self.assertEqual(val, f"{rnd_sig}@{self.rnd_mapset_name}")
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+        # SigSet equals sig. Just testing branching inside.
+        rnd_sigset = tempname(10)
+        sigset_file = f"{self.rnd_mapset_path}/signatures/sigset/{rnd_sigset}"
+        f = open(sigset_file, "w")
+        f.write("A sigset file")
+        f.close()
+        sigset_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIGSET, self.rnd_mapset_name, ctypes.byref(sigset_list)
+        )
+        os.remove(sigset_file)
+        self.assertEqual(ret, 1)
+        val = utils.decode(sigset_list[0])
+        self.assertEqual(val, f"{rnd_sigset}@{self.rnd_mapset_name}")
+        I_free_signatures_list(ret, ctypes.byref(sigset_list))
+
+    def test_multiple_sigs(self):
+        # Should result into a multiple sigs returned
+        rnd_sig1 = tempname(10)
+        sig_file1 = f"{self.rnd_mapset_path}/signatures/sig/{rnd_sig1}"
+        f = open(sig_file1, "w")
+        f.write("A sig file")
+        f.close()
+        rnd_sig2 = tempname(10)
+        sig_file2 = f"{self.rnd_mapset_path}/signatures/sig/{rnd_sig2}"
+        f = open(sig_file2, "w")
+        f.write("A sig file")
+        f.close()
+        # POINTER(POINTER(c_char))
+        sig_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIG, self.rnd_mapset_name, ctypes.byref(sig_list)
+        )
+        os.remove(sig_file1)
+        os.remove(sig_file2)
+        self.assertEqual(ret, 2)
+        golden = (
+            f"{rnd_sig1}@{self.rnd_mapset_name}",
+            f"{rnd_sig2}@{self.rnd_mapset_name}",
+        )
+        self.assertIn(utils.decode(sig_list[0]), golden)
+        self.assertIn(utils.decode(sig_list[1]), golden)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+        # Ditto for sigset
+        rnd_sigset1 = tempname(10)
+        sigset_file1 = f"{self.rnd_mapset_path}/signatures/sigset/{rnd_sigset1}"
+        f = open(sigset_file1, "w")
+        f.write("A sigset file")
+        f.close()
+        rnd_sigset2 = tempname(10)
+        sigset_file2 = f"{self.rnd_mapset_path}/signatures/sigset/{rnd_sigset2}"
+        f = open(sigset_file2, "w")
+        f.write("A sigset file")
+        f.close()
+        sigset_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIGSET, self.rnd_mapset_name, ctypes.byref(sigset_list)
+        )
+        os.remove(sigset_file1)
+        os.remove(sigset_file2)
+        self.assertEqual(ret, 2)
+        golden = (
+            f"{rnd_sigset1}@{self.rnd_mapset_name}",
+            f"{rnd_sigset2}@{self.rnd_mapset_name}",
+        )
+        self.assertIn(utils.decode(sigset_list[0]), golden)
+        self.assertIn(utils.decode(sigset_list[1]), golden)
+        I_free_signatures_list(ret, ctypes.byref(sigset_list))
+
+    def test_multiple_sigs_multiple_mapsets(self):
+        # Test searching in multiple mapsets. Identical to SIGSET case
+        rnd_sig1 = tempname(10)
+        sig_file1 = f"{self.rnd_mapset_path}/signatures/sig/{rnd_sig1}"
+        f = open(sig_file1, "w")
+        f.write("A sig file")
+        f.close()
+        rnd_sig2 = tempname(10)
+        sig_file2 = f"{self.mpath}/signatures/sig/{rnd_sig2}"
+        f = open(sig_file2, "w")
+        f.write("A sig file")
+        f.close()
+        self.sigfiles.append(sig_file2)
+        sig_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIG, None, ctypes.byref(sig_list)
+        )
+        # As temporary mapset is not in the search path, there must be
+        # at least one sig file present
+        # There could be more sigs if this is not an empty mapset
+        self.assertTrue(ret >= 1)
+        ret_list = list(map(utils.decode, sig_list[:ret]))
+        golden = (
+            f"{rnd_sig1}@{self.rnd_mapset_name}",
+            f"{rnd_sig2}@{self.mapset_name}",
+        )
+        self.assertIn(golden[1], ret_list)
+        # Temporary mapset is not in the search path:
+        self.assertNotIn(golden[0], ret_list)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+        # Add temporary mapset to search path and re-run test
+        grass.run_command("g.mapsets", mapset=self.rnd_mapset_name, operation="add")
+        # Search path is cached for this run => reset!
+        G_reset_mapsets()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIG, None, ctypes.byref(sig_list)
+        )
+        grass.run_command("g.mapsets", mapset=self.rnd_mapset_name, operation="remove")
+        G_reset_mapsets()
+        os.remove(sig_file1)
+        os.remove(sig_file2)
+        # There could be more sigs if this is not an empty mapset
+        self.assertTrue(ret >= 2)
+        ret_list = list(map(utils.decode, sig_list[:ret]))
+        self.assertIn(golden[0], ret_list)
+        self.assertIn(golden[1], ret_list)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+
+    def test_multiple_sigsets_multiple_mapsets(self):
+        # Test searching in multiple mapsets. Identical to SIG case
+        rnd_sig1 = tempname(10)
+        sig_file1 = f"{self.rnd_mapset_path}/signatures/sigset/{rnd_sig1}"
+        f = open(sig_file1, "w")
+        f.write("A sigset file")
+        f.close()
+        rnd_sig2 = tempname(10)
+        sig_file2 = f"{self.mpath}/signatures/sigset/{rnd_sig2}"
+        f = open(sig_file2, "w")
+        f.write("A sigset file")
+        f.close()
+        self.sigfiles.append(sig_file2)
+        sig_list = self.list_ptr()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIGSET, None, ctypes.byref(sig_list)
+        )
+        # As temporary mapset is not in the search path, there must be
+        # at least one sig file present
+        # There could be more sigs if this is not an empty mapset
+        self.assertTrue(ret >= 1)
+        ret_list = list(map(utils.decode, sig_list[:ret]))
+        golden = (
+            f"{rnd_sig1}@{self.rnd_mapset_name}",
+            f"{rnd_sig2}@{self.mapset_name}",
+        )
+        self.assertIn(golden[1], ret_list)
+        # Temporary mapset is not in the search path:
+        self.assertNotIn(golden[0], ret_list)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+        # Add temporary mapset to search path and re-run test
+        grass.run_command("g.mapsets", mapset=self.rnd_mapset_name, operation="add")
+        # Search path is cached for this run => reset!
+        G_reset_mapsets()
+        ret = I_signatures_list_by_type(
+            I_SIGFILE_TYPE_SIGSET, None, ctypes.byref(sig_list)
+        )
+        grass.run_command("g.mapsets", mapset=self.rnd_mapset_name, operation="remove")
+        G_reset_mapsets()
+        os.remove(sig_file1)
+        os.remove(sig_file2)
+        # There could be more sigs if this is not an empty mapset
+        self.assertTrue(ret >= 2)
+        ret_list = list(map(utils.decode, sig_list[:ret]))
+        self.assertIn(golden[0], ret_list)
+        self.assertIn(golden[1], ret_list)
+        I_free_signatures_list(ret, ctypes.byref(sig_list))
+
+
+if __name__ == "__main__":
+    test()

+ 547 - 0
lib/imagery/testsuite/test_imagery_sigsetfile.py

@@ -0,0 +1,547 @@
+"""Test of imagery library sigset file 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 os
+import stat
+import ctypes
+
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+from grass.script.core import tempname
+from grass.pygrass import utils
+from grass.pygrass.gis import Mapset
+
+from grass.lib.gis import G_mapset_path
+from grass.lib.raster import Rast_write_bandref
+from grass.lib.imagery import (
+    SigSet,
+    I_InitSigSet,
+    I_NewClassSig,
+    I_NewSubSig,
+    I_WriteSigSet,
+    I_ReadSigSet,
+    I_SortSigSetByBandref,
+    I_fopen_sigset_file_new,
+    I_fopen_sigset_file_old,
+    Ref,
+    I_init_group_ref,
+    I_add_file_to_group_ref,
+    I_free_group_ref,
+    String,
+)
+
+
+class SigSetFileTestCase(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
+        cls.mpath = utils.decode(G_mapset_path())
+        cls.mapset_name = Mapset().name
+        cls.sig_name = tempname(10)
+        cls.sigfile_name = f"{cls.mpath}/signatures/sigset/{cls.sig_name}"
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            os.remove(cls.sigfile_name)
+        except OSError:
+            pass
+
+    def test_I_fopen_signature_file_old_fail(self):
+        sigfile = I_fopen_sigset_file_old(tempname(10))
+        self.assertFalse(sigfile)
+
+    def test_roundtrip_sigset_v1_one_band(self):
+        """Test writing and reading back sigset file (v1)
+        with a single band and fully qualified sigfile name"""
+
+        # Create signature struct
+        So = SigSet()
+        I_InitSigSet(ctypes.byref(So), 1)
+        self.assertEqual(So.nbands, 1)
+        I_NewClassSig(ctypes.byref(So))
+        self.assertEqual(So.nclasses, 1)
+        I_NewSubSig(ctypes.byref(So), ctypes.byref(So.ClassSig[0]))
+        self.assertEqual(So.ClassSig[0].nsubclasses, 1)
+
+        # Fill sigset struct with data
+        So.title = String("Signature title")
+        So.bandrefs[0] = ctypes.create_string_buffer(b"The_Doors")
+        So.ClassSig[0].used = 1
+        So.ClassSig[0].classnum = 2
+        So.ClassSig[0].title = String("1st class")
+        So.ClassSig[0].type = 1
+        So.ClassSig[0].SubSig[0].pi = 3.14
+        So.ClassSig[0].SubSig[0].means[0] = 42.42
+        So.ClassSig[0].SubSig[0].R[0][0] = 69.69
+
+        # Write signatures to file
+        p_new_sigfile = I_fopen_sigset_file_new(self.sig_name)
+        sig_stat = os.stat(self.sigfile_name)
+        self.assertTrue(stat.S_ISREG(sig_stat.st_mode))
+        I_WriteSigSet(p_new_sigfile, ctypes.byref(So))
+        self.libc.fclose(p_new_sigfile)
+
+        # Read back from signatures file
+        Sn = SigSet()
+        fq_name = f"{self.sig_name}@{self.mapset_name}"
+        p_old_sigfile = I_fopen_sigset_file_old(fq_name)
+        ret = I_ReadSigSet(p_old_sigfile, ctypes.byref(Sn))
+        self.assertEqual(ret, 1)
+        self.assertEqual(utils.decode(Sn.title), "Signature title")
+        self.assertEqual(Sn.nbands, 1)
+        bandref = utils.decode(ctypes.cast(Sn.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Doors")
+        self.assertEqual(Sn.nclasses, 1)
+        self.assertEqual(Sn.ClassSig[0].nsubclasses, 1)
+        self.assertEqual(Sn.ClassSig[0].used, 1)
+        self.assertEqual(Sn.ClassSig[0].nsubclasses, 1)
+        self.assertEqual(Sn.ClassSig[0].classnum, 2)
+        self.assertEqual(utils.decode(Sn.ClassSig[0].title), "1st class")
+        self.assertEqual(Sn.ClassSig[0].type, 1)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].pi, 3.14)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].means[0], 42.42)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].R[0][0], 69.69)
+
+        # SigSet does not have free function
+
+    def test_read_fail_sigset_v1_one_band(self):
+        """Reading back should fail as band reference exceeds limit"""
+
+        # Create signature struct
+        So = SigSet()
+        I_InitSigSet(ctypes.byref(So), 1)
+        self.assertEqual(So.nbands, 1)
+        I_NewClassSig(ctypes.byref(So))
+        self.assertEqual(So.nclasses, 1)
+        I_NewSubSig(ctypes.byref(So), ctypes.byref(So.ClassSig[0]))
+        self.assertEqual(So.ClassSig[0].nsubclasses, 1)
+
+        # Fill sigset struct with data
+        So.title = String("Signature title")
+        So.bandrefs[0] = ctypes.create_string_buffer(tempname(252).encode())
+        So.ClassSig[0].used = 1
+        So.ClassSig[0].classnum = 2
+        So.ClassSig[0].title = String("1st class")
+        So.ClassSig[0].type = 1
+        So.ClassSig[0].SubSig[0].pi = 3.14
+        So.ClassSig[0].SubSig[0].means[0] = 42.42
+        So.ClassSig[0].SubSig[0].R[0][0] = 69.69
+
+        # Write signatures to file
+        p_new_sigfile = I_fopen_sigset_file_new(self.sig_name)
+        sig_stat = os.stat(self.sigfile_name)
+        self.assertTrue(stat.S_ISREG(sig_stat.st_mode))
+        I_WriteSigSet(p_new_sigfile, ctypes.byref(So))
+        self.libc.fclose(p_new_sigfile)
+
+        # Read back from signatures file
+        Sn = SigSet()
+        I_InitSigSet(ctypes.byref(So), 0)
+        p_old_sigfile = I_fopen_sigset_file_old(self.sig_name)
+        ret = I_ReadSigSet(p_old_sigfile, ctypes.byref(Sn))
+        self.assertEqual(ret, -1)
+
+        # SigSet does not have free function
+
+    def test_roundtrip_sigset_v1_two_bands(self):
+        """Test writing and reading back sigset (v1) with two bands"""
+
+        # Create signature struct
+        So = SigSet()
+        I_InitSigSet(ctypes.byref(So), 2)
+        self.assertEqual(So.nbands, 2)
+        I_NewClassSig(ctypes.byref(So))
+        self.assertEqual(So.nclasses, 1)
+        I_NewSubSig(ctypes.byref(So), ctypes.byref(So.ClassSig[0]))
+        self.assertEqual(So.ClassSig[0].nsubclasses, 1)
+
+        # Fill sigset struct with data
+        So.title = String("Signature title")
+        So.bandrefs[0] = ctypes.create_string_buffer(b"The_Doors")
+        So.bandrefs[1] = ctypes.create_string_buffer(b"The_Who")
+        So.ClassSig[0].used = 1
+        So.ClassSig[0].classnum = 2
+        So.ClassSig[0].title = String("1st class")
+        So.ClassSig[0].type = 1
+        So.ClassSig[0].SubSig[0].pi = 3.14
+        So.ClassSig[0].SubSig[0].means[0] = 42.42
+        So.ClassSig[0].SubSig[0].means[1] = 24.24
+        So.ClassSig[0].SubSig[0].R[0][0] = 69.69
+        So.ClassSig[0].SubSig[0].R[0][1] = 13.37
+        So.ClassSig[0].SubSig[0].R[1][0] = 13.37
+        So.ClassSig[0].SubSig[0].R[1][1] = 21.21
+
+        # Write signatures to file
+        p_new_sigfile = I_fopen_sigset_file_new(self.sig_name)
+        sig_stat = os.stat(self.sigfile_name)
+        self.assertTrue(stat.S_ISREG(sig_stat.st_mode))
+        I_WriteSigSet(p_new_sigfile, ctypes.byref(So))
+        self.libc.fclose(p_new_sigfile)
+
+        # Read back from signatures file
+        Sn = SigSet()
+        p_old_sigfile = I_fopen_sigset_file_old(self.sig_name)
+        ret = I_ReadSigSet(p_old_sigfile, ctypes.byref(Sn))
+        self.assertEqual(ret, 1)
+        self.assertEqual(utils.decode(Sn.title), "Signature title")
+        self.assertEqual(Sn.nbands, 2)
+        bandref = utils.decode(ctypes.cast(Sn.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Doors")
+        bandref = utils.decode(ctypes.cast(Sn.bandrefs[1], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Who")
+        self.assertEqual(Sn.nclasses, 1)
+        self.assertEqual(Sn.ClassSig[0].nsubclasses, 1)
+        self.assertEqual(Sn.ClassSig[0].used, 1)
+        self.assertEqual(Sn.ClassSig[0].nsubclasses, 1)
+        self.assertEqual(Sn.ClassSig[0].classnum, 2)
+        self.assertEqual(utils.decode(Sn.ClassSig[0].title), "1st class")
+        self.assertEqual(Sn.ClassSig[0].type, 1)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].pi, 3.14)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].means[0], 42.42)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].means[1], 24.24)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].R[0][0], 69.69)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].R[0][1], 13.37)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].R[1][0], 13.37)
+        self.assertEqual(Sn.ClassSig[0].SubSig[0].R[1][1], 21.21)
+
+        # SigSet does not have free function
+
+
+class SortSigSetByBandrefTest(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
+        cls.mapset = Mapset().name
+        cls.map1 = tempname(10)
+        cls.bandref1 = "The_Doors"
+        cls.map2 = tempname(10)
+        cls.bandref2 = "The_Who"
+        cls.map3 = tempname(10)
+        cls.use_temp_region()
+        cls.runModule("g.region", n=1, s=0, e=1, w=0, res=1)
+        cls.runModule("r.mapcalc", expression=f"{cls.map1} = 1")
+        cls.runModule("r.mapcalc", expression=f"{cls.map2} = 1")
+        cls.runModule("r.mapcalc", expression=f"{cls.map3} = 1")
+        Rast_write_bandref(cls.map1, cls.bandref1)
+        Rast_write_bandref(cls.map2, cls.bandref2)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.del_temp_region()
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map1)
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map2)
+        cls.runModule("g.remove", flags="f", type="raster", name=cls.map3)
+
+    def test_symmetric_complete_difference(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+
+        # Prepare sigset struct
+        S = SigSet()
+        I_InitSigSet(ctypes.byref(S), 1)
+        self.assertEqual(S.nbands, 1)
+        I_NewClassSig(ctypes.byref(S))
+        self.assertEqual(S.nclasses, 1)
+        I_NewSubSig(ctypes.byref(S), ctypes.byref(S.ClassSig[0]))
+        self.assertEqual(S.ClassSig[0].nsubclasses, 1)
+        S.title = String("Signature title")
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Troggs")
+        S.ClassSig[0].used = 1
+        S.ClassSig[0].classnum = 2
+        S.ClassSig[0].title = String("1st class")
+        S.ClassSig[0].type = 1
+        S.ClassSig[0].SubSig[0].pi = 3.14
+        S.ClassSig[0].SubSig[0].means[0] = 42.42
+        S.ClassSig[0].SubSig[0].R[0][0] = 69.69
+
+        # This should result in two error strings in ret
+        ret = I_SortSigSetByBandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertTrue(bool(ret))
+        sig_err = utils.decode(ctypes.cast(ret[0], ctypes.c_char_p).value)
+        ref_err = utils.decode(ctypes.cast(ret[1], ctypes.c_char_p).value)
+        self.assertEqual(sig_err, "The_Troggs")
+        self.assertEqual(ref_err, "The_Doors")
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        # I_free_sigset is missing
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_asymmetric_complete_difference(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+
+        # Prepare signature struct
+        S = SigSet()
+        I_InitSigSet(ctypes.byref(S), 1)
+        self.assertEqual(S.nbands, 1)
+        I_NewClassSig(ctypes.byref(S))
+        self.assertEqual(S.nclasses, 1)
+        I_NewSubSig(ctypes.byref(S), ctypes.byref(S.ClassSig[0]))
+        self.assertEqual(S.ClassSig[0].nsubclasses, 1)
+        S.title = String("Signature title")
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Troggs")
+        S.ClassSig[0].used = 1
+        S.ClassSig[0].classnum = 2
+        S.ClassSig[0].title = String("1st class")
+        S.ClassSig[0].type = 1
+        S.ClassSig[0].SubSig[0].pi = 3.14
+        S.ClassSig[0].SubSig[0].means[0] = 42.42
+        S.ClassSig[0].SubSig[0].R[0][0] = 69.69
+
+        # This should result in two error strings in ret
+        ret = I_SortSigSetByBandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertTrue(bool(ret))
+        sig_err = utils.decode(ctypes.cast(ret[0], ctypes.c_char_p).value)
+        ref_err = utils.decode(ctypes.cast(ret[1], ctypes.c_char_p).value)
+        self.assertEqual(sig_err, "The_Troggs")
+        self.assertEqual(ref_err, "The_Doors,The_Who")
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_missing_bandref(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+        ret = I_add_file_to_group_ref(self.map3, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 2)
+
+        # Prepare signature struct
+        S = SigSet()
+        I_InitSigSet(ctypes.byref(S), 10)
+        self.assertEqual(S.nbands, 10)
+        I_NewClassSig(ctypes.byref(S))
+        self.assertEqual(S.nclasses, 1)
+        I_NewSubSig(ctypes.byref(S), ctypes.byref(S.ClassSig[0]))
+        self.assertEqual(S.ClassSig[0].nsubclasses, 1)
+        S.title = String("Signature title")
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Who")
+        S.ClassSig[0].used = 1
+        S.ClassSig[0].classnum = 2
+        S.ClassSig[0].title = String("1st class")
+        S.ClassSig[0].type = 1
+        S.ClassSig[0].SubSig[0].pi = 3.14
+        S.ClassSig[0].SubSig[0].means[0] = 42.42
+        S.ClassSig[0].SubSig[0].R[0][0] = 69.69
+
+        # This should result in two error strings in ret
+        ret = I_SortSigSetByBandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertTrue(bool(ret))
+        sig_err = utils.decode(ctypes.cast(ret[0], ctypes.c_char_p).value)
+        ref_err = utils.decode(ctypes.cast(ret[1], ctypes.c_char_p).value)
+        self.assertEqual(
+            sig_err,
+            "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>,<band reference missing>,"
+            + "<band reference missing>",
+        )
+        self.assertEqual(ref_err, "The_Doors,<band reference missing>")
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_single_complete_match(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+
+        # Prepare signature struct
+        S = SigSet()
+        I_InitSigSet(ctypes.byref(S), 1)
+        self.assertEqual(S.nbands, 1)
+        I_NewClassSig(ctypes.byref(S))
+        self.assertEqual(S.nclasses, 1)
+        I_NewSubSig(ctypes.byref(S), ctypes.byref(S.ClassSig[0]))
+        self.assertEqual(S.ClassSig[0].nsubclasses, 1)
+        S.title = String("Signature title")
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Doors")
+        S.ClassSig[0].used = 1
+        S.ClassSig[0].classnum = 2
+        S.ClassSig[0].title = String("1st class")
+        S.ClassSig[0].type = 1
+        S.ClassSig[0].SubSig[0].pi = 3.14
+        S.ClassSig[0].SubSig[0].means[0] = 42.42
+        S.ClassSig[0].SubSig[0].R[0][0] = 69.69
+
+        # This should result in returning NULL
+        ret = I_SortSigSetByBandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertFalse(bool(ret))
+        bandref = utils.decode(ctypes.cast(S.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref, "The_Doors")
+        self.assertEqual(S.ClassSig[0].SubSig[0].pi, 3.14)
+        self.assertEqual(S.ClassSig[0].SubSig[0].means[0], 42.42)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[0][0], 69.69)
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_double_complete_match_reorder(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+
+        # Prepare signature struct
+        S = SigSet()
+        I_InitSigSet(ctypes.byref(S), 2)
+        self.assertEqual(S.nbands, 2)
+        I_NewClassSig(ctypes.byref(S))
+        self.assertEqual(S.nclasses, 1)
+        I_NewSubSig(ctypes.byref(S), ctypes.byref(S.ClassSig[0]))
+        self.assertEqual(S.ClassSig[0].nsubclasses, 1)
+        S.title = String("Signature title")
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Who")
+        S.bandrefs[1] = ctypes.create_string_buffer(b"The_Doors")
+        S.ClassSig[0].used = 1
+        S.ClassSig[0].classnum = 2
+        S.ClassSig[0].title = String("1st class")
+        S.ClassSig[0].type = 1
+        S.ClassSig[0].SubSig[0].pi = 3.14
+        S.ClassSig[0].SubSig[0].means[0] = 42.42
+        S.ClassSig[0].SubSig[0].means[1] = 24.24
+        S.ClassSig[0].SubSig[0].R[0][0] = 69.69
+        S.ClassSig[0].SubSig[0].R[0][1] = 96.96
+        S.ClassSig[0].SubSig[0].R[1][0] = -69.69
+        S.ClassSig[0].SubSig[0].R[1][1] = -96.96
+
+        # This should result in returning NULL
+        ret = I_SortSigSetByBandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertFalse(bool(ret))
+        # Band references and sig items should be swapped
+        # Static items
+        self.assertEqual(S.ClassSig[0].SubSig[0].pi, 3.14)
+        # Reordered items
+        bandref1 = utils.decode(ctypes.cast(S.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref1, "The_Doors")
+        bandref2 = utils.decode(ctypes.cast(S.bandrefs[1], ctypes.c_char_p).value)
+        self.assertEqual(bandref2, "The_Who")
+        self.assertEqual(S.ClassSig[0].SubSig[0].means[0], 24.24)
+        self.assertEqual(S.ClassSig[0].SubSig[0].means[1], 42.42)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[0][0], -96.96)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[0][1], -69.69)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[1][0], 96.96)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[1][1], 69.69)
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+    def test_double_complete_match_same_order(self):
+        # Prepare imagery group reference struct
+        R = Ref()
+        I_init_group_ref(ctypes.byref(R))
+        ret = I_add_file_to_group_ref(self.map2, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 0)
+        ret = I_add_file_to_group_ref(self.map1, self.mapset, ctypes.byref(R))
+        self.assertEqual(ret, 1)
+
+        # Prepare signature struct
+        S = SigSet()
+        I_InitSigSet(ctypes.byref(S), 2)
+        self.assertEqual(S.nbands, 2)
+        I_NewClassSig(ctypes.byref(S))
+        self.assertEqual(S.nclasses, 1)
+        I_NewSubSig(ctypes.byref(S), ctypes.byref(S.ClassSig[0]))
+        self.assertEqual(S.ClassSig[0].nsubclasses, 1)
+        S.title = String("Signature title")
+        S.bandrefs[0] = ctypes.create_string_buffer(b"The_Who")
+        S.bandrefs[1] = ctypes.create_string_buffer(b"The_Doors")
+        S.ClassSig[0].used = 1
+        S.ClassSig[0].classnum = 2
+        S.ClassSig[0].title = String("1st class")
+        S.ClassSig[0].type = 1
+        S.ClassSig[0].SubSig[0].pi = 3.14
+        S.ClassSig[0].SubSig[0].means[0] = 42.42
+        S.ClassSig[0].SubSig[0].means[1] = 24.24
+        S.ClassSig[0].SubSig[0].R[0][0] = 69.69
+        S.ClassSig[0].SubSig[0].R[0][1] = 96.96
+        S.ClassSig[0].SubSig[0].R[1][0] = -69.69
+        S.ClassSig[0].SubSig[0].R[1][1] = -96.96
+
+        # This should result in returning NULL
+        ret = I_SortSigSetByBandref(ctypes.byref(S), ctypes.byref(R))
+        self.assertFalse(bool(ret))
+        # Band references and sig items should not be swapped
+        # Static items
+        self.assertEqual(S.ClassSig[0].SubSig[0].pi, 3.14)
+        # Reordered items
+        bandref1 = utils.decode(ctypes.cast(S.bandrefs[0], ctypes.c_char_p).value)
+        self.assertEqual(bandref1, "The_Who")
+        bandref2 = utils.decode(ctypes.cast(S.bandrefs[1], ctypes.c_char_p).value)
+        self.assertEqual(bandref2, "The_Doors")
+        self.assertEqual(S.ClassSig[0].SubSig[0].means[0], 42.42)
+        self.assertEqual(S.ClassSig[0].SubSig[0].means[1], 24.24)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[0][0], 69.69)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[0][1], 96.96)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[1][0], -69.69)
+        self.assertEqual(S.ClassSig[0].SubSig[0].R[1][1], -96.96)
+
+        # Clean up memory to help track memory leaks when run by valgrind
+        I_free_group_ref(ctypes.byref(R))
+        if ret:
+            if ret[0]:
+                self.libc.free(ret[0])
+            if ret[1]:
+                self.libc.free(ret[1])
+        self.libc.free(ret)
+
+
+if __name__ == "__main__":
+    test()