1"""
2@package psmap.utils
3
4@brief utilities for wxpsmap (classes, functions)
5
6Classes:
7 - utils::Rect2D
8 - utils::Rect2DPP
9 - utils::Rect2DPS
10 - utils::UnitConversion
11
12(C) 2012 by Anna Kratochvilova, and the GRASS Development Team
13This program is free software under the GNU General Public License
14(>=v2). Read the file COPYING that comes with GRASS for details.
15
16@author Anna Kratochvilova <kratochanna gmail.com>
17"""
18import os
19import wx
20import string
21from math import ceil, floor, sin, cos, pi
22
23try:
24    from PIL import Image as PILImage
25    havePILImage = True
26except ImportError:
27    havePILImage = False
28
29import grass.script as grass
30from core.gcmd import RunCommand
31
32
33class Rect2D(wx.Rect2D):
34    """Class representing rectangle with floating point values.
35
36    Overrides wx.Rect2D to unify Rect access methods, which are
37    different (e.g. wx.Rect.GetTopLeft() x wx.Rect2D.GetLeftTop()).
38    More methods can be added depending on needs.
39    """
40
41    def __init__(self, x=0, y=0, width=0, height=0):
42        wx.Rect2D.__init__(self, x=x, y=y, w=width, h=height)
43
44    def GetX(self):
45        return self.x
46
47    def GetY(self):
48        return self.y
49
50    def GetWidth(self):
51        return self.width
52
53    def SetWidth(self, width):
54        self.width = width
55
56    def GetHeight(self):
57        return self.height
58
59    def SetHeight(self, height):
60        self.height = height
61
62
63class Rect2DPP(Rect2D):
64    """Rectangle specified by 2 points (with floating point values).
65
66    :class:`Rect2D`, :class:`Rect2DPS`
67    """
68
69    def __init__(self, topLeft=wx.Point2D(), bottomRight=wx.Point2D()):
70        Rect2D.__init__(self, x=0, y=0, width=0, height=0)
71
72        x1, y1 = topLeft[0], topLeft[1]
73        x2, y2 = bottomRight[0], bottomRight[1]
74
75        self.SetLeft(min(x1, x2))
76        self.SetTop(min(y1, y2))
77        self.SetRight(max(x1, x2))
78        self.SetBottom(max(y1, y2))
79
80
81class Rect2DPS(Rect2D):
82    """Rectangle specified by point and size (with floating point values).
83
84    :class:`Rect2D`, :class:`Rect2DPP`
85    """
86
87    def __init__(self, pos=wx.Point2D(), size=(0, 0)):
88        Rect2D.__init__(
89            self, x=pos[0],
90            y=pos[1],
91            width=size[0],
92            height=size[1])
93
94
95class UnitConversion:
96    """ Class for converting units"""
97
98    def __init__(self, parent=None):
99        self.parent = parent
100        if self.parent:
101            ppi = wx.ClientDC(self.parent).GetPPI()
102        else:
103            ppi = (72, 72)
104        self._unitsPage = {'inch': {'val': 1.0, 'tr': _("inch")},
105                           'point': {'val': 72.0, 'tr': _("point")},
106                           'centimeter': {'val': 2.54, 'tr': _("centimeter")},
107                           'millimeter': {'val': 25.4, 'tr': _("millimeter")}}
108        self._unitsMap = {
109            'meters': {
110                'val': 0.0254,
111                'tr': _("meters")},
112            'kilometers': {
113                'val': 2.54e-5,
114                'tr': _("kilometers")},
115            'feet': {
116                'val': 1. / 12,
117                'tr': _("feet")},
118            'miles': {
119                'val': 1. / 63360,
120                'tr': _("miles")},
121            'nautical miles': {
122                'val': 1 / 72913.386,
123                'tr': _("nautical miles")}}
124
125        self._units = {'pixel': {'val': ppi[0], 'tr': _("pixel")},
126                       'meter': {'val': 0.0254, 'tr': _("meter")},
127                       'nautmiles': {'val': 1 / 72913.386, 'tr': _("nautical miles")},
128                       # like 1 meter, incorrect
129                       'degrees': {'val': 0.0254, 'tr': _("degree")}
130                       }
131        self._units.update(self._unitsPage)
132        self._units.update(self._unitsMap)
133
134    def getPageUnitsNames(self):
135        return sorted(self._unitsPage[unit]['tr']
136                      for unit in self._unitsPage.keys())
137
138    def getMapUnitsNames(self):
139        return sorted(self._unitsMap[unit]['tr']
140                      for unit in self._unitsMap.keys())
141
142    def getAllUnits(self):
143        return sorted(self._units.keys())
144
145    def findUnit(self, name):
146        """Returns unit by its tr. string"""
147        for unit in self._units.keys():
148            if self._units[unit]['tr'] == name:
149                return unit
150        return None
151
152    def findName(self, unit):
153        """Returns tr. string of a unit"""
154        try:
155            return self._units[unit]['tr']
156        except KeyError:
157            return None
158
159    def convert(self, value, fromUnit=None, toUnit=None):
160        return float(
161            value) / self._units[fromUnit]['val'] * self._units[toUnit]['val']
162
163
164def convertRGB(rgb):
165    """Converts wx.Colour(r,g,b,a) to string 'r:g:b' or named color,
166            or named color/r:g:b string to wx.Colour, depending on input"""
167    # transform a wx.Colour tuple into an r:g:b string
168    if isinstance(rgb, wx.Colour):
169        for name, color in grass.named_colors.items():
170            if  rgb.Red() == int(color[0] * 255) and\
171                    rgb.Green() == int(color[1] * 255) and\
172                    rgb.Blue() == int(color[2] * 255):
173                return name
174        return str(rgb.Red()) + ':' + str(rgb.Green()) + ':' + str(rgb.Blue())
175    # transform a GRASS named color or an r:g:b string into a wx.Colour tuple
176    else:
177        color = (int(grass.parse_color(rgb)[0] * 255),
178                 int(grass.parse_color(rgb)[1] * 255),
179                 int(grass.parse_color(rgb)[2] * 255))
180        color = wx.Colour(*color)
181        if color.IsOk():
182            return color
183        else:
184            return None
185
186
187def PaperMapCoordinates(mapInstr, x, y, paperToMap=True, env=None):
188    """Converts paper (inch) coordinates <-> map coordinates.
189
190    :param mapInstr: map frame instruction
191    :param x,y: paper coords in inches or mapcoords in map units
192    :param paperToMap: specify conversion direction
193    """
194    region = grass.region(env=env)
195    mapWidthPaper = mapInstr['rect'].GetWidth()
196    mapHeightPaper = mapInstr['rect'].GetHeight()
197    mapWidthEN = region['e'] - region['w']
198    mapHeightEN = region['n'] - region['s']
199
200    if paperToMap:
201        diffX = x - mapInstr['rect'].GetX()
202        diffY = y - mapInstr['rect'].GetY()
203        diffEW = diffX * mapWidthEN / mapWidthPaper
204        diffNS = diffY * mapHeightEN / mapHeightPaper
205        e = region['w'] + diffEW
206        n = region['n'] - diffNS
207
208        if projInfo()['proj'] == 'll':
209            return e, n
210        else:
211            return int(e), int(n)
212
213    else:
214        diffEW = x - region['w']
215        diffNS = region['n'] - y
216        diffX = mapWidthPaper * diffEW / mapWidthEN
217        diffY = mapHeightPaper * diffNS / mapHeightEN
218        xPaper = mapInstr['rect'].GetX() + diffX
219        yPaper = mapInstr['rect'].GetY() + diffY
220
221        return xPaper, yPaper
222
223
224def AutoAdjust(self, scaleType, rect, env, map=None, mapType=None, region=None):
225    """Computes map scale, center and map frame rectangle to fit region
226    (scale is not fixed)
227    """
228    currRegionDict = {}
229    try:
230        if scaleType == 0 and map:  # automatic, region from raster or vector
231            res = ''
232            if mapType == 'raster':
233                try:
234                    res = grass.read_command("g.region", flags='gu', raster=map, env=env)
235                except grass.ScriptError:
236                    pass
237            elif mapType == 'vector':
238                res = grass.read_command("g.region", flags='gu', vector=map, env=env)
239            currRegionDict = grass.parse_key_val(res, val_type=float)
240        elif scaleType == 1 and region:  # saved region
241            res = grass.read_command("g.region", flags='gu', region=region, env=env)
242            currRegionDict = grass.parse_key_val(res, val_type=float)
243        elif scaleType == 2:  # current region
244            currRegionDict = grass.region(env=None)
245
246        else:
247            return None, None, None
248    # fails after switching location
249    except (grass.ScriptError, grass.CalledModuleError):
250        pass
251
252    if not currRegionDict:
253        return None, None, None
254    rX = rect.x
255    rY = rect.y
256    rW = rect.width
257    rH = rect.height
258    if not hasattr(self, 'unitConv'):
259        self.unitConv = UnitConversion(self)
260    toM = 1
261    if projInfo()['proj'] != 'xy':
262        toM = float(projInfo()['meters'])
263
264    mW = self.unitConv.convert(
265        value=(
266            currRegionDict['e'] -
267            currRegionDict['w']) *
268        toM,
269        fromUnit='meter',
270        toUnit='inch')
271    mH = self.unitConv.convert(
272        value=(
273            currRegionDict['n'] -
274            currRegionDict['s']) *
275        toM,
276        fromUnit='meter',
277        toUnit='inch')
278    scale = min(rW / mW, rH / mH)
279
280    if rW / rH > mW / mH:
281        x = rX - (rH * (mW / mH) - rW) / 2
282        y = rY
283        rWNew = rH * (mW / mH)
284        rHNew = rH
285    else:
286        x = rX
287        y = rY - (rW * (mH / mW) - rH) / 2
288        rHNew = rW * (mH / mW)
289        rWNew = rW
290
291    # center
292    cE = (currRegionDict['w'] + currRegionDict['e']) / 2
293    cN = (currRegionDict['n'] + currRegionDict['s']) / 2
294    return scale, (cE, cN), Rect2D(x, y, rWNew, rHNew)  # inch
295
296
297def SetResolution(dpi, width, height, env):
298    """If resolution is too high, lower it
299
300    :param dpi: max DPI
301    :param width: map frame width
302    :param height: map frame height
303    """
304    region = grass.region(env=env)
305    if region['cols'] > width * dpi or region['rows'] > height * dpi:
306        rows = height * dpi
307        cols = width * dpi
308        env['GRASS_REGION'] = grass.region_env(rows=rows, cols=cols, env=env)
309
310
311def ComputeSetRegion(self, mapDict, env):
312    """Computes and sets region from current scale, map center
313    coordinates and map rectangle
314    """
315
316    if mapDict['scaleType'] == 3:  # fixed scale
317        scale = mapDict['scale']
318
319        if not hasattr(self, 'unitConv'):
320            self.unitConv = UnitConversion(self)
321
322        fromM = 1
323        if projInfo()['proj'] != 'xy':
324            fromM = float(projInfo()['meters'])
325        rectHalfInch = (mapDict['rect'].width / 2, mapDict['rect'].height / 2)
326        rectHalfMeter = (
327            self.unitConv.convert(
328                value=rectHalfInch[0],
329                fromUnit='inch',
330                toUnit='meter') / fromM / scale,
331            self.unitConv.convert(
332                value=rectHalfInch[1],
333                fromUnit='inch',
334                toUnit='meter') / fromM / scale)
335
336        centerE = mapDict['center'][0]
337        centerN = mapDict['center'][1]
338
339        raster = self.instruction.FindInstructionByType('raster')
340        if raster:
341            rasterId = raster.id
342        else:
343            rasterId = None
344
345        if rasterId:
346            env['GRASS_REGION'] = grass.region_env(n=ceil(centerN + rectHalfMeter[1]),
347                                                   s=floor(centerN - rectHalfMeter[1]),
348                                                   e=ceil(centerE + rectHalfMeter[0]),
349                                                   w=floor(centerE - rectHalfMeter[0]),
350                                                   rast=self.instruction[rasterId]['raster'],
351                                                   env=env)
352        else:
353            env['GRASS_REGION'] = grass.region_env(n=ceil(centerN + rectHalfMeter[1]),
354                                                   s=floor(centerN - rectHalfMeter[1]),
355                                                   e=ceil(centerE + rectHalfMeter[0]),
356                                                   w=floor(centerE - rectHalfMeter[0]),
357                                                   env=env)
358
359
360def projInfo():
361    """Return region projection and map units information,
362    taken from render.py
363    """
364
365    projinfo = dict()
366
367    ret = RunCommand('g.proj', read=True, flags='p')
368
369    if not ret:
370        return projinfo
371
372    for line in ret.splitlines():
373        if ':' in line:
374            key, val = line.split(':')
375            projinfo[key.strip()] = val.strip()
376        elif "XY location (unprojected)" in line:
377            projinfo['proj'] = 'xy'
378            projinfo['units'] = ''
379            break
380
381    return projinfo
382
383
384def GetMapBounds(filename, env, portrait=True):
385    """Run ps.map -b to get information about map bounding box
386
387    :param filename: psmap input file
388    :param env: enironment with GRASS_REGION defined
389    :param portrait: page orientation"""
390    orient = ''
391    if not portrait:
392        orient = 'r'
393    try:
394        bb = list(map(float,
395                    grass.read_command(
396                    'ps.map',
397                    flags='b' +
398                    orient,
399                    quiet=True,
400                    input=filename, env=env).strip().split('=')[1].split(',')))
401    except (grass.ScriptError, IndexError):
402        GError(message=_("Unable to run `ps.map -b`"))
403        return None
404    return Rect2D(bb[0], bb[3], bb[2] - bb[0], bb[1] - bb[3])
405
406
407def getRasterType(map):
408    """Returns type of raster map (CELL, FCELL, DCELL)"""
409    if map is None:
410        map = ''
411    file = grass.find_file(name=map, element='cell')
412    if file.get('file'):
413        rasterType = grass.raster_info(map)['datatype']
414        return rasterType
415    else:
416        return None
417
418
419def BBoxAfterRotation(w, h, angle):
420    """Compute bounding box or rotated rectangle
421
422    :param w: rectangle width
423    :param h: rectangle height
424    :param angle: angle (0, 360) in degrees
425    """
426    angleRad = angle / 180. * pi
427    ct = cos(angleRad)
428    st = sin(angleRad)
429
430    hct = h * ct
431    wct = w * ct
432    hst = h * st
433    wst = w * st
434    y = x = 0
435
436    if 0 < angle <= 90:
437        y_min = y
438        y_max = y + hct + wst
439        x_min = x - hst
440        x_max = x + wct
441    elif 90 < angle <= 180:
442        y_min = y + hct
443        y_max = y + wst
444        x_min = x - hst + wct
445        x_max = x
446    elif 180 < angle <= 270:
447        y_min = y + wst + hct
448        y_max = y
449        x_min = x + wct
450        x_max = x - hst
451    elif 270 < angle <= 360:
452        y_min = y + wst
453        y_max = y + hct
454        x_min = x
455        x_max = x + wct - hst
456
457    width = int(ceil(abs(x_max) + abs(x_min)))
458    height = int(ceil(abs(y_max) + abs(y_min)))
459    return width, height
460
461# hack for Windows, loading EPS works only on Unix
462# these functions are taken from EpsImagePlugin.py
463
464
465def loadPSForWindows(self):
466    # Load EPS via Ghostscript
467    if not self.tile:
468        return
469    self.im = GhostscriptForWindows(self.tile, self.size, self.fp)
470    self.mode = self.im.mode
471    self.size = self.im.size
472    self.tile = []
473
474
475def GhostscriptForWindows(tile, size, fp):
476    """Render an image using Ghostscript (Windows only)"""
477    # Unpack decoder tile
478    decoder, tile, offset, data = tile[0]
479    length, bbox = data
480
481    import tempfile
482    import os
483
484    file = tempfile.mkstemp()[1]
485
486    # Build ghostscript command - for Windows
487    command = ["gswin32c",
488               "-q",                    # quite mode
489               "-g%dx%d" % size,        # set output geometry (pixels)
490               "-dNOPAUSE -dSAFER",     # don't pause between pages, safe mode
491               "-sDEVICE=ppmraw",       # ppm driver
492               "-sOutputFile=%s" % file  # output file
493               ]
494
495    command = string.join(command)
496
497    # push data through ghostscript
498    try:
499        gs = os.popen(command, "w")
500        # adjust for image origin
501        if bbox[0] != 0 or bbox[1] != 0:
502            gs.write("%d %d translate\n" % (-bbox[0], -bbox[1]))
503        fp.seek(offset)
504        while length > 0:
505            s = fp.read(8192)
506            if not s:
507                break
508            length = length - len(s)
509            gs.write(s)
510        status = gs.close()
511        if status:
512            raise IOError("gs failed (status %d)" % status)
513        im = PILImage.core.open_ppm(file)
514
515    finally:
516        try:
517            os.unlink(file)
518        except:
519            pass
520
521    return im
522