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