123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- """!
- @package psmap.utils
- @brief utilities for wxpsmap (classes, functions)
- Classes:
- - utils::Rect2D
- - utils::Rect2DPP
- - utils::Rect2DPS
- - utils::UnitConversion
- (C) 2012 by Anna Kratochvilova, 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 Anna Kratochvilova <kratochanna gmail.com>
- """
- import os
- import wx
- import string
- from math import ceil, floor, sin, cos, pi
- try:
- import Image as PILImage
- havePILImage = True
- except ImportError:
- havePILImage = False
- import grass.script as grass
- from core.gcmd import RunCommand
- from core.utils import _
- class Rect2D(wx.Rect2D):
- """!Class representing rectangle with floating point values.
- Overrides wx.Rect2D to unify Rect access methods, which are
- different (e.g. wx.Rect.GetTopLeft() x wx.Rect2D.GetLeftTop()).
- More methods can be added depending on needs.
- """
- def __init__(self, x = 0, y = 0, width = 0, height = 0):
- wx.Rect2D.__init__(self, x = x, y = y, w = width, h = height)
- def GetX(self):
- return self.x
-
- def GetY(self):
- return self.y
- def GetWidth(self):
- return self.width
- def SetWidth(self, width):
- self.width = width
-
- def GetHeight(self):
- return self.height
- def SetHeight(self, height):
- self.height = height
- class Rect2DPP(Rect2D):
- """!Rectangle specified by 2 points (with floating point values).
- @see Rect2D, Rect2DPS
- """
- def __init__(self, topLeft = wx.Point2D(), bottomRight = wx.Point2D()):
- Rect2D.__init__(self, x = 0, y = 0, width = 0, height = 0)
- x1, y1 = topLeft[0], topLeft[1]
- x2, y2 = bottomRight[0], bottomRight[1]
- self.SetLeft(min(x1, x2))
- self.SetTop(min(y1, y2))
- self.SetRight(max(x1, x2))
- self.SetBottom(max(y1, y2))
- class Rect2DPS(Rect2D):
- """!Rectangle specified by point and size (with floating point values).
- @see Rect2D, Rect2DPP
- """
- def __init__(self, pos = wx.Point2D(), size = (0, 0)):
- Rect2D.__init__(self, x = pos[0], y = pos[1], width = size[0], height = size[1])
- class UnitConversion:
- """! Class for converting units"""
- def __init__(self, parent = None):
- self.parent = parent
- if self.parent:
- ppi = wx.ClientDC(self.parent).GetPPI()
- else:
- ppi = (72, 72)
- self._unitsPage = { 'inch' : {'val': 1.0, 'tr' : _("inch")},
- 'point' : {'val': 72.0, 'tr' : _("point")},
- 'centimeter' : {'val': 2.54, 'tr' : _("centimeter")},
- 'millimeter' : {'val': 25.4, 'tr' : _("millimeter")}}
- self._unitsMap = { 'meters' : {'val': 0.0254, 'tr' : _("meters")},
- 'kilometers' : {'val': 2.54e-5, 'tr' : _("kilometers")},
- 'feet' : {'val': 1./12, 'tr' : _("feet")},
- 'miles' : {'val': 1./63360, 'tr' : _("miles")},
- 'nautical miles': {'val': 1/72913.386, 'tr' : _("nautical miles")}}
- self._units = { 'pixel' : {'val': ppi[0], 'tr' : _("pixel")},
- 'meter' : {'val': 0.0254, 'tr' : _("meter")},
- 'nautmiles' : {'val': 1/72913.386, 'tr' :_("nautical miles")},
- 'degrees' : {'val': 0.0254 , 'tr' : _("degree")} #like 1 meter, incorrect
- }
- self._units.update(self._unitsPage)
- self._units.update(self._unitsMap)
- def getPageUnitsNames(self):
- return sorted(self._unitsPage[unit]['tr'] for unit in self._unitsPage.keys())
-
- def getMapUnitsNames(self):
- return sorted(self._unitsMap[unit]['tr'] for unit in self._unitsMap.keys())
-
- def getAllUnits(self):
- return sorted(self._units.keys())
-
- def findUnit(self, name):
- """!Returns unit by its tr. string"""
- for unit in self._units.keys():
- if self._units[unit]['tr'] == name:
- return unit
- return None
-
- def findName(self, unit):
- """!Returns tr. string of a unit"""
- try:
- return self._units[unit]['tr']
- except KeyError:
- return None
-
- def convert(self, value, fromUnit = None, toUnit = None):
- return float(value)/self._units[fromUnit]['val']*self._units[toUnit]['val']
- def convertRGB(rgb):
- """!Converts wx.Colour(r,g,b,a) to string 'r:g:b' or named color,
- or named color/r:g:b string to wx.Colour, depending on input"""
- # transform a wx.Colour tuple into an r:g:b string
- if type(rgb) == wx.Colour:
- for name, color in grass.named_colors.items():
- if rgb.Red() == int(color[0] * 255) and\
- rgb.Green() == int(color[1] * 255) and\
- rgb.Blue() == int(color[2] * 255):
- return name
- return str(rgb.Red()) + ':' + str(rgb.Green()) + ':' + str(rgb.Blue())
- # transform a GRASS named color or an r:g:b string into a wx.Colour tuple
- else:
- color = (grass.parse_color(rgb)[0]*255,
- grass.parse_color(rgb)[1]*255,
- grass.parse_color(rgb)[2]*255)
- color = wx.Colour(*color)
- if color.IsOk():
- return color
- else:
- return None
-
-
- def PaperMapCoordinates(mapInstr, x, y, paperToMap = True):
- """!Converts paper (inch) coordinates <-> map coordinates.
- @param mapInstr map frame instruction
- @param x,y paper coords in inches or mapcoords in map units
- @param paperToMap specify conversion direction
- """
- region = grass.region()
- mapWidthPaper = mapInstr['rect'].GetWidth()
- mapHeightPaper = mapInstr['rect'].GetHeight()
- mapWidthEN = region['e'] - region['w']
- mapHeightEN = region['n'] - region['s']
- if paperToMap:
- diffX = x - mapInstr['rect'].GetX()
- diffY = y - mapInstr['rect'].GetY()
- diffEW = diffX * mapWidthEN / mapWidthPaper
- diffNS = diffY * mapHeightEN / mapHeightPaper
- e = region['w'] + diffEW
- n = region['n'] - diffNS
- if projInfo()['proj'] == 'll':
- return e, n
- else:
- return int(e), int(n)
- else:
- diffEW = x - region['w']
- diffNS = region['n'] - y
- diffX = mapWidthPaper * diffEW / mapWidthEN
- diffY = mapHeightPaper * diffNS / mapHeightEN
- xPaper = mapInstr['rect'].GetX() + diffX
- yPaper = mapInstr['rect'].GetY() + diffY
- return xPaper, yPaper
- def AutoAdjust(self, scaleType, rect, map = None, mapType = None, region = None):
- """!Computes map scale, center and map frame rectangle to fit region (scale is not fixed)"""
- currRegionDict = {}
- if scaleType == 0 and map:# automatic, region from raster or vector
- res = ''
- if mapType == 'raster':
- try:
- res = grass.read_command("g.region", flags = 'gu', rast = map)
- except grass.ScriptError:
- pass
- elif mapType == 'vector':
- res = grass.read_command("g.region", flags = 'gu', vect = map)
- currRegionDict = grass.parse_key_val(res, val_type = float)
- elif scaleType == 1 and region: # saved region
- res = grass.read_command("g.region", flags = 'gu', region = region)
- currRegionDict = grass.parse_key_val(res, val_type = float)
- elif scaleType == 2: # current region
- env = grass.gisenv()
- windFilePath = os.path.join(env['GISDBASE'], env['LOCATION_NAME'], env['MAPSET'], 'WIND')
- try:
- windFile = open(windFilePath, 'r').read()
- except IOError:
- currRegionDict = grass.region()
- regionDict = grass.parse_key_val(windFile, sep = ':', val_type = float)
- region = grass.read_command("g.region", flags = 'gu', n = regionDict['north'], s = regionDict['south'],
- e = regionDict['east'], w = regionDict['west'])
- currRegionDict = grass.parse_key_val(region, val_type = float)
-
- else:
- return None, None, None
-
- if not currRegionDict:
- return None, None, None
- rX = rect.x
- rY = rect.y
- rW = rect.width
- rH = rect.height
- if not hasattr(self, 'unitConv'):
- self.unitConv = UnitConversion(self)
- toM = 1
- if projInfo()['proj'] != 'xy':
- toM = float(projInfo()['meters'])
- mW = self.unitConv.convert(value = (currRegionDict['e'] - currRegionDict['w']) * toM, fromUnit = 'meter', toUnit = 'inch')
- mH = self.unitConv.convert(value = (currRegionDict['n'] - currRegionDict['s']) * toM, fromUnit = 'meter', toUnit = 'inch')
- scale = min(rW/mW, rH/mH)
-
- if rW/rH > mW/mH:
- x = rX - (rH*(mW/mH) - rW)/2
- y = rY
- rWNew = rH*(mW/mH)
- rHNew = rH
- else:
- x = rX
- y = rY - (rW*(mH/mW) - rH)/2
- rHNew = rW*(mH/mW)
- rWNew = rW
- # center
- cE = (currRegionDict['w'] + currRegionDict['e'])/2
- cN = (currRegionDict['n'] + currRegionDict['s'])/2
- return scale, (cE, cN), Rect2D(x, y, rWNew, rHNew) #inch
- def SetResolution(dpi, width, height):
- """!If resolution is too high, lower it
-
- @param dpi max DPI
- @param width map frame width
- @param height map frame height
- """
- region = grass.region()
- if region['cols'] > width * dpi or region['rows'] > height * dpi:
- rows = height * dpi
- cols = width * dpi
- RunCommand('g.region', rows = rows, cols = cols)
-
- def ComputeSetRegion(self, mapDict):
- """!Computes and sets region from current scale, map center coordinates and map rectangle"""
- if mapDict['scaleType'] == 3: # fixed scale
- scale = mapDict['scale']
-
- if not hasattr(self, 'unitConv'):
- self.unitConv = UnitConversion(self)
-
- fromM = 1
- if projInfo()['proj'] != 'xy':
- fromM = float(projInfo()['meters'])
- rectHalfInch = (mapDict['rect'].width/2, mapDict['rect'].height/2)
- rectHalfMeter = (self.unitConv.convert(value = rectHalfInch[0], fromUnit = 'inch', toUnit = 'meter')/ fromM /scale,
- self.unitConv.convert(value = rectHalfInch[1], fromUnit = 'inch', toUnit = 'meter')/ fromM /scale)
-
- centerE = mapDict['center'][0]
- centerN = mapDict['center'][1]
-
- raster = self.instruction.FindInstructionByType('raster')
- if raster:
- rasterId = raster.id
- else:
- rasterId = None
- if rasterId:
- RunCommand('g.region', n = ceil(centerN + rectHalfMeter[1]),
- s = floor(centerN - rectHalfMeter[1]),
- e = ceil(centerE + rectHalfMeter[0]),
- w = floor(centerE - rectHalfMeter[0]),
- rast = self.instruction[rasterId]['raster'])
- else:
- RunCommand('g.region', n = ceil(centerN + rectHalfMeter[1]),
- s = floor(centerN - rectHalfMeter[1]),
- e = ceil(centerE + rectHalfMeter[0]),
- w = floor(centerE - rectHalfMeter[0]))
-
- def projInfo():
- """!Return region projection and map units information,
- taken from render.py"""
-
- projinfo = dict()
-
- ret = RunCommand('g.proj', read = True, flags = 'p')
-
- if not ret:
- return projinfo
-
- for line in ret.splitlines():
- if ':' in line:
- key, val = line.split(':')
- projinfo[key.strip()] = val.strip()
- elif "XY location (unprojected)" in line:
- projinfo['proj'] = 'xy'
- projinfo['units'] = ''
- break
-
- return projinfo
- def GetMapBounds(filename, portrait = True):
- """!Run ps.map -b to get information about map bounding box
-
- @param filename psmap input file
- @param portrait page orientation"""
- orient = ''
- if not portrait:
- orient = 'r'
- try:
- bb = map(float, grass.read_command('ps.map',
- flags = 'b' + orient,
- quiet = True,
- input = filename).strip().split('=')[1].split(','))
- except (grass.ScriptError, IndexError):
- GError(message = _("Unable to run `ps.map -b`"))
- return None
- return Rect2D(bb[0], bb[3], bb[2] - bb[0], bb[1] - bb[3])
- def getRasterType(map):
- """!Returns type of raster map (CELL, FCELL, DCELL)"""
- if map is None:
- map = ''
- file = grass.find_file(name = map, element = 'cell')
- if file['file']:
- rasterType = grass.raster_info(map)['datatype']
- return rasterType
- else:
- return None
-
- def PilImageToWxImage(pilImage, copyAlpha = True):
- """!Convert PIL image to wx.Image
-
- Based on http://wiki.wxpython.org/WorkingWithImages
- """
- hasAlpha = pilImage.mode[-1] == 'A'
- if copyAlpha and hasAlpha : # Make sure there is an alpha layer copy.
- wxImage = wx.EmptyImage( *pilImage.size )
- pilImageCopyRGBA = pilImage.copy()
- pilImageCopyRGB = pilImageCopyRGBA.convert('RGB') # RGBA --> RGB
- pilImageRgbData = pilImageCopyRGB.tostring()
- wxImage.SetData(pilImageRgbData)
- wxImage.SetAlphaData(pilImageCopyRGBA.tostring()[3::4]) # Create layer and insert alpha values.
- else : # The resulting image will not have alpha.
- wxImage = wx.EmptyImage(*pilImage.size)
- pilImageCopy = pilImage.copy()
- pilImageCopyRGB = pilImageCopy.convert('RGB') # Discard any alpha from the PIL image.
- pilImageRgbData = pilImageCopyRGB.tostring()
- wxImage.SetData(pilImageRgbData)
- return wxImage
- def BBoxAfterRotation(w, h, angle):
- """!Compute bounding box or rotated rectangle
-
- @param w rectangle width
- @param h rectangle height
- @param angle angle (0, 360) in degrees
- """
- angleRad = angle / 180. * pi
- ct = cos(angleRad)
- st = sin(angleRad)
-
- hct = h * ct
- wct = w * ct
- hst = h * st
- wst = w * st
- y = x = 0
-
- if 0 < angle <= 90:
- y_min = y
- y_max = y + hct + wst
- x_min = x - hst
- x_max = x + wct
- elif 90 < angle <= 180:
- y_min = y + hct
- y_max = y + wst
- x_min = x - hst + wct
- x_max = x
- elif 180 < angle <= 270:
- y_min = y + wst + hct
- y_max = y
- x_min = x + wct
- x_max = x - hst
- elif 270 < angle <= 360:
- y_min = y + wst
- y_max = y + hct
- x_min = x
- x_max = x + wct - hst
-
- width = int(ceil(abs(x_max) + abs(x_min)))
- height = int(ceil(abs(y_max) + abs(y_min)))
- return width, height
- # hack for Windows, loading EPS works only on Unix
- # these functions are taken from EpsImagePlugin.py
- def loadPSForWindows(self):
- # Load EPS via Ghostscript
- if not self.tile:
- return
- self.im = GhostscriptForWindows(self.tile, self.size, self.fp)
- self.mode = self.im.mode
- self.size = self.im.size
- self.tile = []
- def GhostscriptForWindows(tile, size, fp):
- """Render an image using Ghostscript (Windows only)"""
- # Unpack decoder tile
- decoder, tile, offset, data = tile[0]
- length, bbox = data
- import tempfile, os
- file = tempfile.mkstemp()[1]
- # Build ghostscript command - for Windows
- command = ["gswin32c",
- "-q", # quite mode
- "-g%dx%d" % size, # set output geometry (pixels)
- "-dNOPAUSE -dSAFER", # don't pause between pages, safe mode
- "-sDEVICE=ppmraw", # ppm driver
- "-sOutputFile=%s" % file # output file
- ]
- command = string.join(command)
- # push data through ghostscript
- try:
- gs = os.popen(command, "w")
- # adjust for image origin
- if bbox[0] != 0 or bbox[1] != 0:
- gs.write("%d %d translate\n" % (-bbox[0], -bbox[1]))
- fp.seek(offset)
- while length > 0:
- s = fp.read(8192)
- if not s:
- break
- length = length - len(s)
- gs.write(s)
- status = gs.close()
- if status:
- raise IOError("gs failed (status %d)" % status)
- im = PILImage.core.open_ppm(file)
- finally:
- try: os.unlink(file)
- except: pass
- return im
|