1# -*- coding: utf-8 -*-
2"""
3Created on Thu Jun 28 17:44:14 2012
4
5@author: pietro
6"""
7import ctypes
8from operator import itemgetter
9
10import grass.lib.raster as libraster
11from grass.exceptions import ImplementationError
12
13from grass.pygrass.errors import GrassError
14from grass.pygrass.utils import decode
15from grass.pygrass.raster.raster_type import TYPE as RTYPE
16
17
18class Category(list):
19    """
20    I would like to add the following functions:
21
22    Getting the umber of cats:
23    Rast_number_of_cats() <- Important for ith access
24
25    Getting and setting the title:
26    Rast_get_cats_title()
27    Rast_set_cats_title()
28
29    Do not use these functions for category access:
30    Rast_get_cat()
31    and the specialized types for CELL, FCELL and DCELL.
32    Since these functions are working on hidden static buffer.
33
34    Use the ith-get methods:
35    Rast_get_ith_c_cat()
36    Rast_get_ith_f_cat()
37    Rast_get_ith_d_cat()
38
39    This can be implemented using an iterator too. So that the category object
40    provides the [] access operator to the categories, returning a tuple
41    (label, min, max).
42    Using this, the category object must be aware of its raster map type.
43
44    Set categories using:
45    Rast_set_c_cat()
46    Rast_set_f_cat()
47    Rast_set_d_cat()
48
49    Misc:
50    Rast_sort_cats()
51    Rast_copy_cats() <- This should be wrapped so that categories from an
52    existing Python category class are copied.
53
54    """
55    def __init__(self, name, mapset='', mtype='CELL', *args, **kargs):
56        self.name = name
57        self.mapset = mapset
58        self.c_cats = libraster.Categories()
59        libraster.Rast_init_cats("", ctypes.byref(self.c_cats))
60        self._mtype = mtype
61        self._gtype = None if mtype is None else RTYPE[mtype]['grass type']
62        super(Category, self).__init__(*args, **kargs)
63
64    def _get_mtype(self):
65        return self._mtype
66
67    def _set_mtype(self, mtype):
68        if mtype.upper() not in ('CELL', 'FCELL', 'DCELL'):
69            raise ValueError(_("Raster type: {0} not supported".format(mtype)))
70        self._mtype = mtype
71        self._gtype = RTYPE[self.mtype]['grass type']
72
73    mtype = property(fget=_get_mtype, fset=_set_mtype,
74                     doc="Set or obtain raster data type")
75
76    def _get_title(self):
77        return libraster.Rast_get_cats_title(ctypes.byref(self.c_cats))
78
79    def _set_title(self, newtitle):
80        return libraster.Rast_set_cats_title(newtitle,
81                                             ctypes.byref(self.c_cats))
82
83    title = property(fget=_get_title, fset=_set_title,
84                     doc="Set or obtain raster title")
85
86    def __str__(self):
87        return self.__repr__()
88
89    def __list__(self):
90        cats = []
91        for cat in self.__iter__():
92            cats.append(cat)
93        return cats
94
95    def __dict__(self):
96        diz = dict()
97        for cat in self.__iter__():
98            label, min_cat, max_cat = cat
99            diz[(min_cat, max_cat)] = label
100        return diz
101
102    def __repr__(self):
103        cats = []
104        for cat in self.__iter__():
105            cats.append(repr(cat))
106        return "[{0}]".format(',\n '.join(cats))
107
108    def _chk_index(self, index):
109        if type(index) == str:
110            try:
111                index = self.labels().index(index)
112            except ValueError:
113                raise KeyError(index)
114        return index
115
116    def _chk_value(self, value):
117        if type(value) == tuple:
118            length = len(value)
119            if length == 2:
120                label, min_cat = value
121                value = (label, min_cat, None)
122            elif length < 2 or length > 3:
123                raise TypeError('Tuple with a length that is not supported.')
124        else:
125            raise TypeError('Only Tuple are supported.')
126        return value
127
128    def __getitem__(self, index):
129        return super(Category, self).__getitem__(self._chk_index(index))
130
131    def __setitem__(self, index, value):
132        return super(Category, self).__setitem__(self._chk_index(index),
133                                                 self._chk_value(value))
134
135    def _get_c_cat(self, index):
136        """Returns i-th description and i-th data range from the list of
137        category descriptions with corresponding data ranges. end points of
138        data interval.
139
140        Rast_get_ith_cat(const struct Categories * 	pcats,
141                         int 	i,
142                         void * 	rast1,
143                         void * 	rast2,
144                         RASTER_MAP_TYPE 	data_type
145                         )
146        """
147        min_cat = ctypes.pointer(RTYPE[self.mtype]['grass def']())
148        max_cat = ctypes.pointer(RTYPE[self.mtype]['grass def']())
149        lab = decode(libraster.Rast_get_ith_cat(ctypes.byref(self.c_cats),
150                                                index,
151                                                ctypes.cast(min_cat, ctypes.c_void_p),
152                                                ctypes.cast(max_cat, ctypes.c_void_p),
153                                                self._gtype))
154        # Manage C function Errors
155        if lab == '':
156            raise GrassError(_("Error executing: Rast_get_ith_cat"))
157        if max_cat.contents.value == min_cat.contents.value:
158            max_cat = None
159        else:
160            max_cat = max_cat.contents.value
161        return lab, min_cat.contents.value, max_cat
162
163    def _set_c_cat(self, label, min_cat, max_cat=None):
164        """Adds the label for range min through max in category structure cats.
165
166        int Rast_set_cat(const void * 	rast1,
167                         const void * 	rast2,
168                         const char * 	label,
169                         struct Categories * 	pcats,
170                         RASTER_MAP_TYPE 	data_type
171                         )
172        """
173        max_cat = min_cat if max_cat is None else max_cat
174        min_cat = ctypes.pointer(RTYPE[self.mtype]['grass def'](min_cat))
175        max_cat = ctypes.pointer(RTYPE[self.mtype]['grass def'](max_cat))
176        err = libraster.Rast_set_cat(ctypes.cast(min_cat, ctypes.c_void_p),
177                                     ctypes.cast(max_cat, ctypes.c_void_p),
178                                     label,
179                                     ctypes.byref(self.c_cats), self._gtype)
180        # Manage C function Errors
181        if err == 1:
182            return None
183        elif err == 0:
184            raise GrassError(_("Null value detected"))
185        elif err == -1:
186            raise GrassError(_("Error executing: Rast_set_cat"))
187
188    def __del__(self):
189        libraster.Rast_free_cats(ctypes.byref(self.c_cats))
190
191    def get_cat(self, index):
192        return self.__getitem__(index)
193
194    def set_cat(self, index, value):
195        if index is None:
196            self.append(value)
197        elif index < self.__len__():
198            self.__setitem__(index, value)
199        else:
200            raise TypeError("Index outside range.")
201
202    def reset(self):
203        for i in range(len(self) - 1, -1, -1):
204            del(self[i])
205        libraster.Rast_init_cats("", ctypes.byref(self.c_cats))
206
207    def _read_cats(self):
208        """Copy from the C struct to the list"""
209        for i in range(self.c_cats.ncats):
210            self.append(self._get_c_cat(i))
211
212    def _write_cats(self):
213        """Copy from the list data to the C struct"""
214        # reset only the C struct
215        libraster.Rast_init_cats("", ctypes.byref(self.c_cats))
216        # write to the c struct
217        for cat in self.__iter__():
218            label, min_cat, max_cat = cat
219            if max_cat is None:
220                max_cat = min_cat
221            self._set_c_cat(label, min_cat, max_cat)
222
223    def read(self):
224        """Read categories from a raster map
225
226        The category file for raster map name in mapset is read into the
227        cats structure. If there is an error reading the category file,
228        a diagnostic message is printed.
229
230        int Rast_read_cats(const char * 	name,
231                           const char * 	mapset,
232                           struct Categories * 	pcats
233                           )
234        """
235        self.reset()
236        err = libraster.Rast_read_cats(self.name, self.mapset,
237                                       ctypes.byref(self.c_cats))
238        if err == -1:
239            raise GrassError("Can not read the categories.")
240        # copy from C struct to list
241        self._read_cats()
242
243    def write(self):
244        """Writes the category file for the raster map name in the current
245           mapset from the cats structure.
246
247        void Rast_write_cats(const char * 	name,
248                             struct Categories * 	cats
249                             )
250        """
251        # copy from list to C struct
252        self._write_cats()
253        # write to the map
254        libraster.Rast_write_cats(self.name, ctypes.byref(self.c_cats))
255
256    def copy(self, category):
257        """Copy from another Category class
258
259        :param category: Category class to be copied
260        :type category: Category object
261        """
262        libraster.Rast_copy_cats(ctypes.byref(self.c_cats),     # to
263                                 ctypes.byref(category.c_cats))  # from
264        self._read_cats()
265
266    def ncats(self):
267        return self.__len__()
268
269    def set_cats_fmt(self, fmt, m1, a1, m2, a2):
270        """Not implemented yet.
271        void Rast_set_cats_fmt()
272        """
273        # TODO: add
274        raise ImplementationError("set_cats_fmt() is not implemented yet.")
275
276    def read_rules(self, filename, sep=':'):
277        """Copy categories from a rules file, default separetor is ':', the
278        columns must be: min and/or max and label. ::
279
280            1:forest
281            2:road
282            3:urban
283
284            0.:0.5:forest
285            0.5:1.0:road
286            1.0:1.5:urban
287
288        :param str filename: the name of file with categories rules
289        :param str sep: the separator used to divide values and category
290
291        """
292        self.reset()
293        with open(filename, 'r') as f:
294            for row in f.readlines():
295                cat = row.strip().split(sep)
296                if len(cat) == 2:
297                    label, min_cat = cat
298                    max_cat = None
299                elif len(cat) == 3:
300                    label, min_cat, max_cat = cat
301                else:
302                    raise TypeError("Row length is greater than 3")
303                self.append((label, min_cat, max_cat))
304
305    def write_rules(self, filename, sep=':'):
306        """Copy categories from a rules file, default separetor is ':', the
307        columns must be: min and/or max and label. ::
308
309            1:forest
310            2:road
311            3:urban
312
313            0.:0.5:forest
314            0.5:1.0:road
315            1.0:1.5:urban
316
317        :param str filename: the name of file with categories rules
318        :param str sep: the separator used to divide values and category
319        """
320        with open(filename, 'w') as f:
321            cats = []
322            for cat in self.__iter__():
323                if cat[-1] is None:
324                    cat = cat[:-1]
325                cats.append(sep.join([str(i) for i in cat]))
326            f.write('\n'.join(cats))
327
328    def sort(self):
329        libraster.Rast_sort_cats(ctypes.byref(self.c_cats))
330
331    def labels(self):
332        return list(map(itemgetter(0), self))
333