""" @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 """ import os import wx import string from math import ceil, floor, sin, cos, pi try: from PIL 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). :class:`Rect2D`, :class:`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). :class:`Rect2D`, :class:`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")}, # like 1 meter, incorrect 'degrees': {'val': 0.0254, 'tr': _("degree")} } 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 isinstance(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', raster=map) except grass.ScriptError: pass elif mapType == 'vector': res = grass.read_command("g.region", flags='gu', vector=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 currRegionDict = grass.region() 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 = list(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 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 import 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