1"""
2@package core.utils
3
4@brief Misc utilities for wxGUI
5
6(C) 2007-2015 by the GRASS Development Team
7
8This program is free software under the GNU General Public License
9(>=v2). Read the file COPYING that comes with GRASS for details.
10
11@author Martin Landa <landa.martin gmail.com>
12@author Jachym Cepicky
13"""
14
15import os
16import sys
17import platform
18import string
19import glob
20import shlex
21import re
22import inspect
23import six
24
25from grass.script import core as grass
26from grass.script import task as gtask
27from grass.exceptions import OpenError
28
29from core.gcmd import RunCommand
30from core.debug import Debug
31from core.globalvar import ETCDIR, wxPythonPhoenix
32
33def cmp(a, b):
34    """cmp function"""
35    return ((a > b) - (a < b))
36
37
38def normalize_whitespace(text):
39    """Remove redundant whitespace from a string"""
40    return (' ').join(text.split())
41
42
43def split(s):
44    """Platform spefic shlex.split"""
45    try:
46        if sys.platform == "win32":
47            return shlex.split(s.replace('\\', r'\\'))
48        else:
49            return shlex.split(s)
50    except ValueError as e:
51        sys.stderr.write(_("Syntax error: %s") % e)
52
53    return []
54
55
56def GetTempfile(pref=None):
57    """Creates GRASS temporary file using defined prefix.
58
59    .. todo::
60        Fix path on MS Windows/MSYS
61
62    :param pref: prefer the given path
63
64    :return: Path to file name (string) or None
65    """
66    ret = RunCommand('g.tempfile',
67                     read=True,
68                     pid=os.getpid())
69
70    tempfile = ret.splitlines()[0].strip()
71
72    # FIXME
73    # ugly hack for MSYS (MS Windows)
74    if platform.system() == 'Windows':
75        tempfile = tempfile.replace("/", "\\")
76    try:
77        path, file = os.path.split(tempfile)
78        if pref:
79            return os.path.join(pref, file)
80        else:
81            return tempfile
82    except:
83        return None
84
85
86def GetLayerNameFromCmd(dcmd, fullyQualified=False, param=None,
87                        layerType=None):
88    """Get map name from GRASS command
89
90    Parameter dcmd can be modified when first parameter is not
91    defined.
92
93    :param dcmd: GRASS command (given as list)
94    :param fullyQualified: change map name to be fully qualified
95    :param param: params directory
96    :param str layerType: check also layer type ('raster', 'vector',
97                          'raster_3d', ...)
98
99    :return: tuple (name, found)
100    """
101    mapname = ''
102    found = True
103
104    if len(dcmd) < 1:
105        return mapname, False
106
107    if 'd.grid' == dcmd[0]:
108        mapname = 'grid'
109    elif 'd.geodesic' in dcmd[0]:
110        mapname = 'geodesic'
111    elif 'd.rhumbline' in dcmd[0]:
112        mapname = 'rhumb'
113    elif 'd.graph' in dcmd[0]:
114        mapname = 'graph'
115    else:
116        params = list()
117        for idx in range(len(dcmd)):
118            try:
119                p, v = dcmd[idx].split('=', 1)
120            except ValueError:
121                continue
122
123            if p == param:
124                params = [(idx, p, v)]
125                break
126
127            # this does not use types, just some (incomplete subset of?) names
128            if p in ('map', 'input', 'layer',
129                     'red', 'blue', 'green',
130                     'hue', 'saturation', 'intensity',
131                     'shade', 'labels'):
132                params.append((idx, p, v))
133
134        if len(params) < 1:
135            if len(dcmd) > 1:
136                i = 1
137                while i < len(dcmd):
138                    if '=' not in dcmd[i] and not dcmd[i].startswith('-'):
139                        task = gtask.parse_interface(dcmd[0])
140                        # this expects the first parameter to be the right one
141                        p = task.get_options()['params'][0].get('name', '')
142                        params.append((i, p, dcmd[i]))
143                        break
144                    i += 1
145            else:
146                return mapname, False
147
148        if len(params) < 1:
149            return mapname, False
150
151        # need to add mapset for all maps
152        mapsets = {}
153        for i, p, v in params:
154            if p == 'layer':
155                continue
156            mapname = v
157            mapset = ''
158            if fullyQualified and '@' not in mapname:
159                if layerType in ('raster', 'vector',
160                                 'raster_3d', 'rgb', 'his'):
161                    try:
162                        if layerType in ('raster', 'rgb', 'his'):
163                            findType = 'cell'
164                        elif layerType == 'raster_3d':
165                            findType = 'grid3'
166                        else:
167                            findType = layerType
168                        mapset = grass.find_file(
169                            mapname, element=findType)['mapset']
170                    except AttributeError:  # not found
171                        return '', False
172                    if not mapset:
173                        found = False
174                else:
175                    mapset = ''  # grass.gisenv()['MAPSET']
176            mapsets[i] = mapset
177
178        # update dcmd
179        for i, p, v in params:
180            if p == 'layer':
181                continue
182            dcmd[i] = p + '=' + v
183            if i in mapsets and mapsets[i]:
184                dcmd[i] += '@' + mapsets[i]
185
186        maps = list()
187        ogr = False
188        for i, p, v in params:
189            if v.lower().rfind('@ogr') > -1:
190                ogr = True
191            if p == 'layer' and not ogr:
192                continue
193            maps.append(dcmd[i].split('=', 1)[1])
194
195        mapname = '\n'.join(maps)
196
197    return mapname, found
198
199
200def GetValidLayerName(name):
201    """Make layer name SQL compliant, based on G_str_to_sql()
202
203    .. todo::
204        Better use directly Ctypes to reuse venerable libgis C fns...
205    """
206    retName = name.strip()
207
208    # check if name is fully qualified
209    if '@' in retName:
210        retName, mapset = retName.split('@')
211    else:
212        mapset = None
213
214    cIdx = 0
215    retNameList = list(retName)
216    for c in retNameList:
217        if not (c >= 'A' and c <= 'Z') and \
218                not (c >= 'a' and c <= 'z') and \
219                not (c >= '0' and c <= '9'):
220            retNameList[cIdx] = '_'
221        cIdx += 1
222    retName = ''.join(retNameList)
223
224    if not (retName[0] >= 'A' and retName[0] <= 'Z') and \
225            not (retName[0] >= 'a' and retName[0] <= 'z'):
226        retName = 'x' + retName[1:]
227
228    if mapset:
229        retName = retName + '@' + mapset
230
231    return retName
232
233
234def ListOfCatsToRange(cats):
235    """Convert list of category number to range(s)
236
237    Used for example for d.vect cats=[range]
238
239    :param cats: category list
240
241    :return: category range string
242    :return: '' on error
243    """
244
245    catstr = ''
246
247    try:
248        cats = list(map(int, cats))
249    except:
250        return catstr
251
252    i = 0
253    while i < len(cats):
254        next = 0
255        j = i + 1
256        while j < len(cats):
257            if cats[i + next] == cats[j] - 1:
258                next += 1
259            else:
260                break
261            j += 1
262
263        if next > 1:
264            catstr += '%d-%d,' % (cats[i], cats[i + next])
265            i += next + 1
266        else:
267            catstr += '%d,' % (cats[i])
268            i += 1
269
270    return catstr.strip(',')
271
272
273def ListOfMapsets(get='ordered'):
274    """Get list of available/accessible mapsets
275
276    :param str get: method ('all', 'accessible', 'ordered')
277
278    :return: list of mapsets
279    :return: None on error
280    """
281    mapsets = []
282
283    if get == 'all' or get == 'ordered':
284        ret = RunCommand('g.mapsets',
285                         read=True,
286                         quiet=True,
287                         flags='l',
288                         sep='newline')
289
290        if ret:
291            mapsets = ret.splitlines()
292            ListSortLower(mapsets)
293        else:
294            return None
295
296    if get == 'accessible' or get == 'ordered':
297        ret = RunCommand('g.mapsets',
298                         read=True,
299                         quiet=True,
300                         flags='p',
301                         sep='newline')
302        if ret:
303            if get == 'accessible':
304                mapsets = ret.splitlines()
305            else:
306                mapsets_accessible = ret.splitlines()
307                for mapset in mapsets_accessible:
308                    mapsets.remove(mapset)
309                mapsets = mapsets_accessible + mapsets
310        else:
311            return None
312
313    return mapsets
314
315
316def ListSortLower(list):
317    """Sort list items (not case-sensitive)"""
318    list.sort(key=lambda x: x.lower())
319
320
321def GetVectorNumberOfLayers(vector):
322    """Get list of all vector layers"""
323    layers = list()
324    if not vector:
325        return layers
326
327    fullname = grass.find_file(name=vector, element='vector')['fullname']
328    if not fullname:
329        Debug.msg(
330            5,
331            "utils.GetVectorNumberOfLayers(): vector map '%s' not found" %
332            vector)
333        return layers
334
335    ret, out, msg = RunCommand('v.category',
336                               getErrorMsg=True,
337                               read=True,
338                               input=fullname,
339                               option='layers')
340    if ret != 0:
341        sys.stderr.write(
342            _("Vector map <%(map)s>: %(msg)s\n") %
343            {'map': fullname, 'msg': msg})
344        return layers
345    else:
346        Debug.msg(1, "GetVectorNumberOfLayers(): ret %s" % ret)
347
348    for layer in out.splitlines():
349        layers.append(layer)
350
351    Debug.msg(3, "utils.GetVectorNumberOfLayers(): vector=%s -> %s" %
352              (fullname, ','.join(layers)))
353
354    return layers
355
356
357def Deg2DMS(lon, lat, string=True, hemisphere=True, precision=3):
358    """Convert deg value to dms string
359
360    :param lon: longitude (x)
361    :param lat: latitude (y)
362    :param string: True to return string otherwise tuple
363    :param hemisphere: print hemisphere
364    :param precision: seconds precision
365
366    :return: DMS string or tuple of values
367    :return: empty string on error
368    """
369    try:
370        flat = float(lat)
371        flon = float(lon)
372    except ValueError:
373        if string:
374            return ''
375        else:
376            return None
377
378    # fix longitude
379    while flon > 180.0:
380        flon -= 360.0
381    while flon < -180.0:
382        flon += 360.0
383
384    # hemisphere
385    if hemisphere:
386        if flat < 0.0:
387            flat = abs(flat)
388            hlat = 'S'
389        else:
390            hlat = 'N'
391
392        if flon < 0.0:
393            hlon = 'W'
394            flon = abs(flon)
395        else:
396            hlon = 'E'
397    else:
398        flat = abs(flat)
399        flon = abs(flon)
400        hlon = ''
401        hlat = ''
402
403    slat = __ll_parts(flat, precision=precision)
404    slon = __ll_parts(flon, precision=precision)
405
406    if string:
407        return slon + hlon + '; ' + slat + hlat
408
409    return (slon + hlon, slat + hlat)
410
411
412def DMS2Deg(lon, lat):
413    """Convert dms value to deg
414
415    :param lon: longitude (x)
416    :param lat: latitude (y)
417
418    :return: tuple of converted values
419    :return: ValueError on error
420    """
421    x = __ll_parts(lon, reverse=True)
422    y = __ll_parts(lat, reverse=True)
423
424    return (x, y)
425
426
427def __ll_parts(value, reverse=False, precision=3):
428    """Converts deg to d:m:s string
429
430    :param value: value to be converted
431    :param reverse: True to convert from d:m:s to deg
432    :param precision: seconds precision (ignored if reverse is True)
433
434    :return: converted value (string/float)
435    :return: ValueError on error (reverse == True)
436    """
437    if not reverse:
438        if value == 0.0:
439            return '%s%.*f' % ('00:00:0', precision, 0.0)
440
441        d = int(int(value))
442        m = int((value - d) * 60)
443        s = ((value - d) * 60 - m) * 60
444        if m < 0:
445            m = '00'
446        elif m < 10:
447            m = '0' + str(m)
448        else:
449            m = str(m)
450        if s < 0:
451            s = '00.0000'
452        elif s < 10.0:
453            s = '0%.*f' % (precision, s)
454        else:
455            s = '%.*f' % (precision, s)
456
457        return str(d) + ':' + m + ':' + s
458    else:  # -> reverse
459        try:
460            d, m, s = value.split(':')
461            hs = s[-1]
462            s = s[:-1]
463        except ValueError:
464            try:
465                d, m = value.split(':')
466                hs = m[-1]
467                m = m[:-1]
468                s = '0.0'
469            except ValueError:
470                try:
471                    d = value
472                    hs = d[-1]
473                    d = d[:-1]
474                    m = '0'
475                    s = '0.0'
476                except ValueError:
477                    raise ValueError
478
479        if hs not in ('N', 'S', 'E', 'W'):
480            raise ValueError
481
482        coef = 1.0
483        if hs in ('S', 'W'):
484            coef = -1.0
485
486        fm = int(m) / 60.0
487        fs = float(s) / (60 * 60)
488
489        return coef * (float(d) + fm + fs)
490
491
492def GetCmdString(cmd):
493    """Get GRASS command as string.
494
495    :param cmd: GRASS command given as tuple
496
497    :return: command string
498    """
499    return ' '.join(gtask.cmdtuple_to_list(cmd))
500
501
502def PathJoin(*args):
503    """Check path created by os.path.join"""
504    path = os.path.join(*args)
505    if platform.system() == 'Windows' and \
506            '/' in path:
507        return path[1].upper() + ':\\' + path[3:].replace('/', '\\')
508
509    return path
510
511
512def ReadEpsgCodes():
513    """Read EPSG codes with g.proj
514
515    :return: dictionary of EPSG code
516    """
517    epsgCodeDict = dict()
518
519    ret = RunCommand('g.proj',
520                     read=True,
521                     list_codes="EPSG")
522
523    for line in ret.splitlines():
524        code, descr, params = line.split("|")
525        epsgCodeDict[int(code)] = (descr, params)
526
527    return epsgCodeDict
528
529
530def ReprojectCoordinates(coord, projOut, projIn=None, flags=''):
531    """Reproject coordinates
532
533    :param coord: coordinates given as tuple
534    :param projOut: output projection
535    :param projIn: input projection (use location projection settings)
536
537    :return: reprojected coordinates (returned as tuple)
538    """
539    coors = RunCommand('m.proj',
540                       flags=flags,
541                       input='-',
542                       proj_in=projIn,
543                       proj_out=projOut,
544                       sep=';',
545                       stdin='%f;%f' % (coord[0], coord[1]),
546                       read=True)
547    if coors:
548        coors = coors.split(';')
549        e = coors[0]
550        n = coors[1]
551        try:
552            proj = projOut.split(' ')[0].split('=')[1]
553        except IndexError:
554            proj = ''
555        if proj in ('ll', 'latlong', 'longlat') and 'd' not in flags:
556            return (proj, (e, n))
557        else:
558            try:
559                return (proj, (float(e), float(n)))
560            except ValueError:
561                return (None, None)
562
563    return (None, None)
564
565
566def GetListOfLocations(dbase):
567    """Get list of GRASS locations in given dbase
568
569    :param dbase: GRASS database path
570
571    :return: list of locations (sorted)
572    """
573    listOfLocations = list()
574
575    try:
576        for location in glob.glob(os.path.join(dbase, "*")):
577            try:
578                if os.path.join(
579                        location, "PERMANENT") in glob.glob(
580                        os.path.join(location, "*")):
581                    listOfLocations.append(os.path.basename(location))
582            except:
583                pass
584    except (UnicodeEncodeError, UnicodeDecodeError) as e:
585        raise e
586
587    ListSortLower(listOfLocations)
588
589    return listOfLocations
590
591
592def GetListOfMapsets(dbase, location, selectable=False):
593    """Get list of mapsets in given GRASS location
594
595    :param dbase: GRASS database path
596    :param location: GRASS location
597    :param selectable: True to get list of selectable mapsets, otherwise all
598
599    :return: list of mapsets - sorted (PERMANENT first)
600    """
601    listOfMapsets = list()
602
603    if selectable:
604        ret = RunCommand('g.mapset',
605                         read=True,
606                         flags='l',
607                         location=location,
608                         dbase=dbase)
609
610        if not ret:
611            return listOfMapsets
612
613        for line in ret.rstrip().splitlines():
614            listOfMapsets += line.split(' ')
615    else:
616        for mapset in glob.glob(os.path.join(dbase, location, "*")):
617            if os.path.isdir(mapset) and os.path.isfile(
618                    os.path.join(dbase, location, mapset, "WIND")):
619                listOfMapsets.append(os.path.basename(mapset))
620
621    ListSortLower(listOfMapsets)
622    return listOfMapsets
623
624
625def GetColorTables():
626    """Get list of color tables"""
627    ret = RunCommand('r.colors',
628                     read=True,
629                     flags='l')
630    if not ret:
631        return list()
632
633    return ret.splitlines()
634
635
636def _getGDALFormats():
637    """Get dictionary of avaialble GDAL drivers"""
638    try:
639        ret = grass.read_command('r.in.gdal',
640                                 quiet=True,
641                                 flags='f')
642    except:
643        ret = None
644
645    return _parseFormats(ret), _parseFormats(ret, writableOnly=True)
646
647
648def _getOGRFormats():
649    """Get dictionary of avaialble OGR drivers"""
650    try:
651        ret = grass.read_command('v.in.ogr',
652                                 quiet=True,
653                                 flags='f')
654    except:
655        ret = None
656
657    return _parseFormats(ret), _parseFormats(ret, writableOnly=True)
658
659
660def _parseFormats(output, writableOnly=False):
661    """Parse r.in.gdal/v.in.ogr -f output"""
662    formats = {'file': list(),
663               'database': list(),
664               'protocol': list()
665               }
666
667    if not output:
668        return formats
669
670    patt = None
671    if writableOnly:
672        patt = re.compile('\(rw\+?\)$', re.IGNORECASE)
673
674    for line in output.splitlines():
675        key, name = map(lambda x: x.strip(), line.strip().split(':', 1))
676
677        if writableOnly and not patt.search(key):
678            continue
679
680        if name in ('Memory', 'Virtual Raster', 'In Memory Raster'):
681            continue
682        if name in ('PostgreSQL', 'SQLite',
683                    'ODBC', 'ESRI Personal GeoDatabase',
684                    'Rasterlite',
685                    'PostGIS WKT Raster driver',
686                    'PostGIS Raster driver',
687                    'CouchDB',
688                    'MSSQLSpatial',
689                    'FileGDB'):
690            formats['database'].append(name)
691        elif name in ('GeoJSON',
692                      'OGC Web Coverage Service',
693                      'OGC Web Map Service',
694                      'WFS',
695                      'GeoRSS',
696                      'HTTP Fetching Wrapper'):
697            formats['protocol'].append(name)
698        else:
699            formats['file'].append(name)
700
701    for items in six.itervalues(formats):
702        items.sort()
703
704    return formats
705
706formats = None
707
708
709def GetFormats(writableOnly=False):
710    """Get GDAL/OGR formats"""
711    global formats
712    if not formats:
713        gdalAll, gdalWritable = _getGDALFormats()
714        ogrAll, ogrWritable = _getOGRFormats()
715        formats = {
716            'all': {
717                'gdal': gdalAll,
718                'ogr': ogrAll,
719            },
720            'writable': {
721                'gdal': gdalWritable,
722                'ogr': ogrWritable,
723            },
724        }
725
726    if writableOnly:
727        return formats['writable']
728
729    return formats['all']
730
731
732rasterFormatExtension = {
733    'GeoTIFF': 'tif',
734    'Erdas Imagine Images (.img)': 'img',
735    'Ground-based SAR Applications Testbed File Format (.gff)': 'gff',
736    'Arc/Info Binary Grid': 'adf',
737    'Portable Network Graphics': 'png',
738    'JPEG JFIF': 'jpg',
739    'Japanese DEM (.mem)': 'mem',
740    'Graphics Interchange Format (.gif)': 'gif',
741    'X11 PixMap Format': 'xpm',
742    'MS Windows Device Independent Bitmap': 'bmp',
743    'SPOT DIMAP': 'dim',
744    'RadarSat 2 XML Product': 'xml',
745    'EarthWatch .TIL': 'til',
746    'ERMapper .ers Labelled': 'ers',
747    'ERMapper Compressed Wavelets': 'ecw',
748    'GRIdded Binary (.grb)': 'grb',
749    'EUMETSAT Archive native (.nat)': 'nat',
750    'Idrisi Raster A.1': 'rst',
751    'Golden Software ASCII Grid (.grd)': 'grd',
752    'Golden Software Binary Grid (.grd)': 'grd',
753    'Golden Software 7 Binary Grid (.grd)': 'grd',
754    'R Object Data Store': 'r',
755    'USGS DOQ (Old Style)': 'doq',
756    'USGS DOQ (New Style)': 'doq',
757    'ENVI .hdr Labelled': 'hdr',
758    'ESRI .hdr Labelled': 'hdr',
759    'Generic Binary (.hdr Labelled)': 'hdr',
760    'PCI .aux Labelled': 'aux',
761    'EOSAT FAST Format': 'fst',
762    'VTP .bt (Binary Terrain) 1.3 Format': 'bt',
763    'FARSITE v.4 Landscape File (.lcp)': 'lcp',
764    'Swedish Grid RIK (.rik)': 'rik',
765    'USGS Optional ASCII DEM (and CDED)': 'dem',
766    'Northwood Numeric Grid Format .grd/.tab': '',
767    'Northwood Classified Grid Format .grc/.tab': '',
768    'ARC Digitized Raster Graphics': 'arc',
769    'Magellan topo (.blx)': 'blx',
770    'SAGA GIS Binary Grid (.sdat)': 'sdat',
771    'GeoPackage (.gpkg)': 'gpkg'
772}
773
774
775vectorFormatExtension = {
776    'ESRI Shapefile': 'shp',
777    'GeoPackage': 'gpkg',
778    'UK .NTF': 'ntf',
779    'SDTS': 'ddf',
780    'DGN': 'dgn',
781    'VRT': 'vrt',
782    'REC': 'rec',
783    'BNA': 'bna',
784    'CSV': 'csv',
785    'GML': 'gml',
786    'GPX': 'gpx',
787    'KML': 'kml',
788    'GMT': 'gmt',
789    'PGeo': 'mdb',
790    'XPlane': 'dat',
791    'AVCBin': 'adf',
792    'AVCE00': 'e00',
793    'DXF': 'dxf',
794    'Geoconcept': 'gxt',
795    'GeoRSS': 'xml',
796    'GPSTrackMaker': 'gtm',
797    'VFK': 'vfk',
798    'SVG': 'svg'
799}
800
801
802def GetSettingsPath():
803    """Get full path to the settings directory
804    """
805    try:
806        verFd = open(os.path.join(ETCDIR, "VERSIONNUMBER"))
807        version = int(verFd.readlines()[0].split(' ')[0].split('.')[0])
808    except (IOError, ValueError, TypeError, IndexError) as e:
809        sys.exit(
810            _("ERROR: Unable to determine GRASS version. Details: %s") %
811            e)
812
813    verFd.close()
814
815    # keep location of settings files rc and wx in sync with lib/init/grass.py
816    if sys.platform == 'win32':
817        return os.path.join(os.getenv('APPDATA'), 'GRASS%d' % version)
818
819    return os.path.join(os.getenv('HOME'), '.grass%d' % version)
820
821
822def StoreEnvVariable(key, value=None, envFile=None):
823    """Store environmental variable
824
825    If value is not given (is None) then environmental variable is
826    unset.
827
828    :param key: env key
829    :param value: env value
830    :param envFile: path to the environmental file (None for default location)
831    """
832    windows = sys.platform == 'win32'
833    if not envFile:
834        gVersion = grass.version()['version'].split('.', 1)[0]
835        if not windows:
836            envFile = os.path.join(
837                os.getenv('HOME'), '.grass%s' %
838                gVersion, 'bashrc')
839        else:
840            envFile = os.path.join(
841                os.getenv('APPDATA'), 'GRASS%s' %
842                gVersion, 'env.bat')
843
844    # read env file
845    environ = dict()
846    lineSkipped = list()
847    if os.path.exists(envFile):
848        try:
849            fd = open(envFile)
850        except IOError as e:
851            sys.stderr.write(_("Unable to open file '%s'\n") % envFile)
852            return
853        for line in fd.readlines():
854            line = line.rstrip(os.linesep)
855            try:
856                k, v = map(
857                    lambda x: x.strip(), line.split(
858                        ' ', 1)[1].split(
859                        '=', 1))
860            except Exception as e:
861                sys.stderr.write(_("%s: line skipped - unable to parse '%s'\n"
862                                   "Reason: %s\n") % (envFile, line, e))
863                lineSkipped.append(line)
864                continue
865            if k in environ:
866                sys.stderr.write(_("Duplicated key: %s\n") % k)
867            environ[k] = v
868
869        fd.close()
870
871    # update environmental variables
872    if value is None:
873        if key in environ:
874            del environ[key]
875    else:
876        environ[key] = value
877
878    # write update env file
879    try:
880        fd = open(envFile, 'w')
881    except IOError as e:
882        sys.stderr.write(_("Unable to create file '%s'\n") % envFile)
883        return
884    if windows:
885        expCmd = 'set'
886    else:
887        expCmd = 'export'
888
889    for key, value in six.iteritems(environ):
890        fd.write('%s %s=%s\n' % (expCmd, key, value))
891
892    # write also skipped lines
893    for line in lineSkipped:
894        fd.write(line + os.linesep)
895
896    fd.close()
897
898
899def SetAddOnPath(addonPath=None, key='PATH'):
900    """Set default AddOn path
901
902    :param addonPath: path to addons (None for default)
903    :param key: env key - 'PATH' or 'BASE'
904    """
905    gVersion = grass.version()['version'].split('.', 1)[0]
906    # update env file
907    if not addonPath:
908        if sys.platform != 'win32':
909            addonPath = os.path.join(os.path.join(os.getenv('HOME'),
910                                                  '.grass%s' % gVersion,
911                                                  'addons'))
912        else:
913            addonPath = os.path.join(os.path.join(os.getenv('APPDATA'),
914                                                  'GRASS%s' % gVersion,
915                                                  'addons'))
916
917    StoreEnvVariable(key='GRASS_ADDON_' + key, value=addonPath)
918    os.environ['GRASS_ADDON_' + key] = addonPath
919
920    # update path
921    if addonPath not in os.environ['PATH']:
922        os.environ['PATH'] = addonPath + os.pathsep + os.environ['PATH']
923
924
925# predefined colors and their names
926# must be in sync with lib/gis/color_str.c
927str2rgb = {'aqua': (100, 128, 255),
928           'black': (0, 0, 0),
929           'blue': (0, 0, 255),
930           'brown': (180, 77, 25),
931           'cyan': (0, 255, 255),
932           'gray': (128, 128, 128),
933           'grey': (128, 128, 128),
934           'green': (0, 255, 0),
935           'indigo': (0, 128, 255),
936           'magenta': (255, 0, 255),
937           'orange': (255, 128, 0),
938           'red': (255, 0, 0),
939           'violet': (128, 0, 255),
940           'purple': (128, 0, 255),
941           'white': (255, 255, 255),
942           'yellow': (255, 255, 0)}
943rgb2str = {}
944for (s, r) in str2rgb.items():
945    rgb2str[r] = s
946# ensure that gray value has 'gray' string and not 'grey'
947rgb2str[str2rgb['gray']] = 'gray'
948# purple is defined as nickname for violet in lib/gis
949# (although Wikipedia says that purple is (128, 0, 128))
950# we will prefer the defined color, not nickname
951rgb2str[str2rgb['violet']] = 'violet'
952
953
954def color_resolve(color):
955    if len(color) > 0 and color[0] in "0123456789":
956        rgb = tuple(map(int, color.split(':')))
957        label = color
958    else:
959        # Convert color names to RGB
960        try:
961            rgb = str2rgb[color]
962            label = color
963        except KeyError:
964            rgb = (200, 200, 200)
965            label = _('Select Color')
966    return (rgb, label)
967
968command2ltype = {'d.rast': 'raster',
969                 'd.rast3d': 'raster_3d',
970                 'd.rgb': 'rgb',
971                 'd.his': 'his',
972                 'd.shade': 'shaded',
973                 'd.legend': 'rastleg',
974                 'd.rast.arrow': 'rastarrow',
975                 'd.rast.num': 'rastnum',
976                 'd.rast.leg': 'maplegend',
977                 'd.vect': 'vector',
978                 'd.vect.thematic': 'thememap',
979                 'd.vect.chart': 'themechart',
980                 'd.grid': 'grid',
981                 'd.geodesic': 'geodesic',
982                 'd.rhumbline': 'rhumb',
983                 'd.labels': 'labels',
984                 'd.barscale': 'barscale',
985                 'd.redraw': 'redraw',
986                 'd.wms': 'wms',
987                 'd.histogram': 'histogram',
988                 'd.colortable': 'colortable',
989                 'd.graph': 'graph',
990                 'd.out.file': 'export',
991                 'd.to.rast': 'torast',
992                 'd.text': 'text',
993                 'd.northarrow': 'northarrow',
994                 'd.polar': 'polar',
995                 'd.legend.vect': 'vectleg'
996                 }
997ltype2command = {}
998for (cmd, ltype) in command2ltype.items():
999    ltype2command[ltype] = cmd
1000
1001
1002def GetGEventAttribsForHandler(method, event):
1003    """Get attributes from event, which can be used by handler method.
1004
1005    Be aware of event class attributes.
1006
1007    :param method: handler method (including self arg)
1008    :param event: event
1009
1010    :return: (valid kwargs for method,
1011             list of method's args without default value
1012             which were not found among event attributes)
1013    """
1014    args_spec = inspect.getargspec(method)
1015
1016    args = args_spec[0]
1017
1018    defaults = []
1019    if args_spec[3]:
1020        defaults = args_spec[3]
1021
1022    # number of arguments without def value
1023    req_args = len(args) - 1 - len(defaults)
1024
1025    kwargs = {}
1026    missing_args = []
1027
1028    for i, a in enumerate(args):
1029        if hasattr(event, a):
1030            kwargs[a] = getattr(event, a)
1031        elif i < req_args:
1032            missing_args.append(a)
1033
1034    return kwargs, missing_args
1035
1036
1037def PilImageToWxImage(pilImage, copyAlpha=True):
1038    """Convert PIL image to wx.Image
1039
1040    Based on http://wiki.wxpython.org/WorkingWithImages
1041    """
1042    from gui_core.wrap import EmptyImage
1043    hasAlpha = pilImage.mode[-1] == 'A'
1044    if copyAlpha and hasAlpha:  # Make sure there is an alpha layer copy.
1045        wxImage = EmptyImage(*pilImage.size)
1046        pilImageCopyRGBA = pilImage.copy()
1047        pilImageCopyRGB = pilImageCopyRGBA.convert('RGB')    # RGBA --> RGB
1048        wxImage.SetData(pilImageCopyRGB.tobytes())
1049        # Create layer and insert alpha values.
1050        if wxPythonPhoenix:
1051            wxImage.SetAlpha(pilImageCopyRGBA.tobytes()[3::4])
1052        else:
1053            wxImage.SetAlphaData(pilImageCopyRGBA.tobytes()[3::4])
1054
1055    else:    # The resulting image will not have alpha.
1056        wxImage = EmptyImage(*pilImage.size)
1057        pilImageCopy = pilImage.copy()
1058        # Discard any alpha from the PIL image.
1059        pilImageCopyRGB = pilImageCopy.convert('RGB')
1060        wxImage.SetData(pilImageCopyRGB.tobytes())
1061
1062    return wxImage
1063
1064
1065def autoCropImageFromFile(filename):
1066    """Loads image from file and crops it automatically.
1067
1068    If PIL is not installed, it does not crop it.
1069
1070    :param filename: path to file
1071    :return: wx.Image instance
1072    """
1073    try:
1074        from PIL import Image
1075        pilImage = Image.open(filename)
1076        imageBox = pilImage.getbbox()
1077        cropped = pilImage.crop(imageBox)
1078        return PilImageToWxImage(cropped, copyAlpha=True)
1079    except ImportError:
1080        import wx
1081        return wx.Image(filename)
1082
1083
1084def isInRegion(regionA, regionB):
1085    """Tests if 'regionA' is inside of 'regionB'.
1086
1087    For example, region A is a display region and region B is some reference
1088    region, e.g., a computational region.
1089
1090    >>> displayRegion = {'n': 223900, 's': 217190, 'w': 630780, 'e': 640690}
1091    >>> compRegion = {'n': 228500, 's': 215000, 'w': 630000, 'e': 645000}
1092    >>> isInRegion(displayRegion, compRegion)
1093    True
1094    >>> displayRegion = {'n':226020, 's': 212610, 'w': 626510, 'e': 646330}
1095    >>> isInRegion(displayRegion, compRegion)
1096    False
1097
1098    :param regionA: input region A as dictionary
1099    :param regionB: input region B as dictionary
1100
1101    :return: True if region A is inside of region B
1102    :return: False othewise
1103    """
1104    if regionA['s'] >= regionB['s'] and \
1105            regionA['n'] <= regionB['n'] and \
1106            regionA['w'] >= regionB['w'] and \
1107            regionA['e'] <= regionB['e']:
1108        return True
1109
1110    return False
1111
1112
1113def do_doctest_gettext_workaround():
1114    """Setups environment for doing a doctest with gettext usage.
1115
1116    When using gettext with dynamically defined underscore function
1117    (`_("For translation")`), doctest does not work properly. One option is to
1118    use `import as` instead of dynamically defined underscore function but this
1119    would require change all modules which are used by tested module. This
1120    should be considered for the future. The second option is to define dummy
1121    underscore function and one other function which creates the right
1122    environment to satisfy all. This is done by this function.
1123    """
1124    def new_displayhook(string):
1125        """A replacement for default `sys.displayhook`"""
1126        if string is not None:
1127            sys.stdout.write("%r\n" % (string,))
1128
1129    def new_translator(string):
1130        """A fake gettext underscore function."""
1131        return string
1132
1133    sys.displayhook = new_displayhook
1134
1135    import __builtin__
1136    __builtin__._ = new_translator
1137
1138
1139def doc_test():
1140    """Tests the module using doctest
1141
1142    :return: a number of failed tests
1143    """
1144    import doctest
1145    do_doctest_gettext_workaround()
1146    return doctest.testmod().failed
1147
1148
1149def registerPid(pid):
1150    """Register process id as GUI_PID GRASS variable
1151
1152    :param: pid process id
1153    """
1154    env = grass.gisenv()
1155    guiPid = []
1156    if 'GUI_PID' in env:
1157        guiPid = env['GUI_PID'].split(',')
1158    guiPid.append(str(pid))
1159    grass.run_command('g.gisenv', set='GUI_PID={0}'.format(','.join(guiPid)))
1160
1161
1162def unregisterPid(pid):
1163    """Unregister process id from GUI_PID GRASS variable
1164
1165    :param: pid process id
1166    """
1167    env = grass.gisenv()
1168    if 'GUI_PID' not in env:
1169        return
1170
1171    guiPid = env['GUI_PID'].split(',')
1172    pid = str(os.getpid())
1173    if pid in guiPid:
1174        guiPid.remove(pid)
1175        grass.run_command(
1176            'g.gisenv',
1177            set='GUI_PID={0}'.format(
1178                ','.join(guiPid)))
1179
1180if __name__ == '__main__':
1181    sys.exit(doc_test())
1182