utils.py 15 KB

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