utils.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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. from math import ceil, floor, sin, cos, pi
  17. try:
  18. import Image as PILImage
  19. havePILImage = True
  20. except ImportError:
  21. havePILImage = False
  22. import grass.script as grass
  23. from core.gcmd import RunCommand
  24. class Rect2D(wx.Rect2D):
  25. """!Class representing rectangle with floating point values.
  26. Overrides wx.Rect2D to unify Rect access methods, which are
  27. different (e.g. wx.Rect.GetTopLeft() x wx.Rect2D.GetLeftTop()).
  28. More methods can be added depending on needs.
  29. """
  30. def __init__(self, x = 0, y = 0, width = 0, height = 0):
  31. wx.Rect2D.__init__(self, x = x, y = y, w = width, h = height)
  32. def GetX(self):
  33. return self.x
  34. def GetY(self):
  35. return self.y
  36. def GetWidth(self):
  37. return self.width
  38. def SetWidth(self, width):
  39. self.width = width
  40. def GetHeight(self):
  41. return self.height
  42. def SetHeight(self, height):
  43. self.height = height
  44. class Rect2DPP(Rect2D):
  45. """!Rectangle specified by 2 points (with floating point values).
  46. @see Rect2D, Rect2DPS
  47. """
  48. def __init__(self, topLeft = wx.Point2D(), bottomRight = wx.Point2D()):
  49. Rect2D.__init__(self, x = 0, y = 0, width = 0, height = 0)
  50. x1, y1 = topLeft[0], topLeft[1]
  51. x2, y2 = bottomRight[0], bottomRight[1]
  52. self.SetLeft(min(x1, x2))
  53. self.SetTop(min(y1, y2))
  54. self.SetRight(max(x1, x2))
  55. self.SetBottom(max(y1, y2))
  56. class Rect2DPS(Rect2D):
  57. """!Rectangle specified by point and size (with floating point values).
  58. @see Rect2D, Rect2DPP
  59. """
  60. def __init__(self, pos = wx.Point2D(), size = (0, 0)):
  61. Rect2D.__init__(self, x = pos[0], y = pos[1], width = size[0], height = size[1])
  62. class UnitConversion:
  63. """! Class for converting units"""
  64. def __init__(self, parent = None):
  65. self.parent = parent
  66. if self.parent:
  67. ppi = wx.ClientDC(self.parent).GetPPI()
  68. else:
  69. ppi = (72, 72)
  70. self._unitsPage = { 'inch' : {'val': 1.0, 'tr' : _("inch")},
  71. 'point' : {'val': 72.0, 'tr' : _("point")},
  72. 'centimeter' : {'val': 2.54, 'tr' : _("centimeter")},
  73. 'millimeter' : {'val': 25.4, 'tr' : _("millimeter")}}
  74. self._unitsMap = { 'meters' : {'val': 0.0254, 'tr' : _("meters")},
  75. 'kilometers' : {'val': 2.54e-5, 'tr' : _("kilometers")},
  76. 'feet' : {'val': 1./12, 'tr' : _("feet")},
  77. 'miles' : {'val': 1./63360, 'tr' : _("miles")},
  78. 'nautical miles': {'val': 1/72913.386, 'tr' : _("nautical miles")}}
  79. self._units = { 'pixel' : {'val': ppi[0], 'tr' : _("pixel")},
  80. 'meter' : {'val': 0.0254, 'tr' : _("meter")},
  81. 'nautmiles' : {'val': 1/72913.386, 'tr' :_("nautical miles")},
  82. 'degrees' : {'val': 0.0254 , 'tr' : _("degree")} #like 1 meter, incorrect
  83. }
  84. self._units.update(self._unitsPage)
  85. self._units.update(self._unitsMap)
  86. def getPageUnitsNames(self):
  87. return sorted(self._unitsPage[unit]['tr'] for unit in self._unitsPage.keys())
  88. def getMapUnitsNames(self):
  89. return sorted(self._unitsMap[unit]['tr'] for unit in self._unitsMap.keys())
  90. def getAllUnits(self):
  91. return sorted(self._units.keys())
  92. def findUnit(self, name):
  93. """!Returns unit by its tr. string"""
  94. for unit in self._units.keys():
  95. if self._units[unit]['tr'] == name:
  96. return unit
  97. return None
  98. def findName(self, unit):
  99. """!Returns tr. string of a unit"""
  100. try:
  101. return self._units[unit]['tr']
  102. except KeyError:
  103. return None
  104. def convert(self, value, fromUnit = None, toUnit = None):
  105. return float(value)/self._units[fromUnit]['val']*self._units[toUnit]['val']
  106. def convertRGB(rgb):
  107. """!Converts wx.Colour(r,g,b,a) to string 'r:g:b' or named color,
  108. or named color/r:g:b string to wx.Colour, depending on input"""
  109. # transform a wx.Colour tuple into an r:g:b string
  110. if type(rgb) == wx.Colour:
  111. for name, color in grass.named_colors.items():
  112. if rgb.Red() == int(color[0] * 255) and\
  113. rgb.Green() == int(color[1] * 255) and\
  114. rgb.Blue() == int(color[2] * 255):
  115. return name
  116. return str(rgb.Red()) + ':' + str(rgb.Green()) + ':' + str(rgb.Blue())
  117. # transform a GRASS named color or an r:g:b string into a wx.Colour tuple
  118. else:
  119. color = (grass.parse_color(rgb)[0]*255,
  120. grass.parse_color(rgb)[1]*255,
  121. grass.parse_color(rgb)[2]*255)
  122. color = wx.Colour(*color)
  123. if color.IsOk():
  124. return color
  125. else:
  126. return None
  127. def PaperMapCoordinates(mapInstr, x, y, paperToMap = True):
  128. """!Converts paper (inch) coordinates <-> map coordinates.
  129. @param mapInstr map frame instruction
  130. @param x,y paper coords in inches or mapcoords in map units
  131. @param paperToMap specify conversion direction
  132. """
  133. region = grass.region()
  134. mapWidthPaper = mapInstr['rect'].GetWidth()
  135. mapHeightPaper = mapInstr['rect'].GetHeight()
  136. mapWidthEN = region['e'] - region['w']
  137. mapHeightEN = region['n'] - region['s']
  138. if paperToMap:
  139. diffX = x - mapInstr['rect'].GetX()
  140. diffY = y - mapInstr['rect'].GetY()
  141. diffEW = diffX * mapWidthEN / mapWidthPaper
  142. diffNS = diffY * mapHeightEN / mapHeightPaper
  143. e = region['w'] + diffEW
  144. n = region['n'] - diffNS
  145. if projInfo()['proj'] == 'll':
  146. return e, n
  147. else:
  148. return int(e), int(n)
  149. else:
  150. diffEW = x - region['w']
  151. diffNS = region['n'] - y
  152. diffX = mapWidthPaper * diffEW / mapWidthEN
  153. diffY = mapHeightPaper * diffNS / mapHeightEN
  154. xPaper = mapInstr['rect'].GetX() + diffX
  155. yPaper = mapInstr['rect'].GetY() + diffY
  156. return xPaper, yPaper
  157. def AutoAdjust(self, scaleType, rect, map = None, mapType = None, region = None):
  158. """!Computes map scale, center and map frame rectangle to fit region (scale is not fixed)"""
  159. currRegionDict = {}
  160. if scaleType == 0 and map:# automatic, region from raster or vector
  161. res = ''
  162. if mapType == 'raster':
  163. try:
  164. res = grass.read_command("g.region", flags = 'gu', rast = map)
  165. except grass.ScriptError:
  166. pass
  167. elif mapType == 'vector':
  168. res = grass.read_command("g.region", flags = 'gu', vect = map)
  169. currRegionDict = grass.parse_key_val(res, val_type = float)
  170. elif scaleType == 1 and region: # saved region
  171. res = grass.read_command("g.region", flags = 'gu', region = region)
  172. currRegionDict = grass.parse_key_val(res, val_type = float)
  173. elif scaleType == 2: # current region
  174. env = grass.gisenv()
  175. windFilePath = os.path.join(env['GISDBASE'], env['LOCATION_NAME'], env['MAPSET'], 'WIND')
  176. try:
  177. windFile = open(windFilePath, 'r').read()
  178. except IOError:
  179. currRegionDict = grass.region()
  180. regionDict = grass.parse_key_val(windFile, sep = ':', val_type = float)
  181. region = grass.read_command("g.region", flags = 'gu', n = regionDict['north'], s = regionDict['south'],
  182. e = regionDict['east'], w = regionDict['west'])
  183. currRegionDict = grass.parse_key_val(region, val_type = float)
  184. else:
  185. return None, None, None
  186. if not currRegionDict:
  187. return None, None, None
  188. rX = rect.x
  189. rY = rect.y
  190. rW = rect.width
  191. rH = rect.height
  192. if not hasattr(self, 'unitConv'):
  193. self.unitConv = UnitConversion(self)
  194. toM = 1
  195. if projInfo()['proj'] != 'xy':
  196. toM = float(projInfo()['meters'])
  197. mW = self.unitConv.convert(value = (currRegionDict['e'] - currRegionDict['w']) * toM, fromUnit = 'meter', toUnit = 'inch')
  198. mH = self.unitConv.convert(value = (currRegionDict['n'] - currRegionDict['s']) * toM, fromUnit = 'meter', toUnit = 'inch')
  199. scale = min(rW/mW, rH/mH)
  200. if rW/rH > mW/mH:
  201. x = rX - (rH*(mW/mH) - rW)/2
  202. y = rY
  203. rWNew = rH*(mW/mH)
  204. rHNew = rH
  205. else:
  206. x = rX
  207. y = rY - (rW*(mH/mW) - rH)/2
  208. rHNew = rW*(mH/mW)
  209. rWNew = rW
  210. # center
  211. cE = (currRegionDict['w'] + currRegionDict['e'])/2
  212. cN = (currRegionDict['n'] + currRegionDict['s'])/2
  213. return scale, (cE, cN), Rect2D(x, y, rWNew, rHNew) #inch
  214. def SetResolution(dpi, width, height):
  215. """!If resolution is too high, lower it
  216. @param dpi max DPI
  217. @param width map frame width
  218. @param height map frame height
  219. """
  220. region = grass.region()
  221. if region['cols'] > width * dpi or region['rows'] > height * dpi:
  222. rows = height * dpi
  223. cols = width * dpi
  224. RunCommand('g.region', rows = rows, cols = cols)
  225. def ComputeSetRegion(self, mapDict):
  226. """!Computes and sets region from current scale, map center coordinates and map rectangle"""
  227. if mapDict['scaleType'] == 3: # fixed scale
  228. scale = mapDict['scale']
  229. if not hasattr(self, 'unitConv'):
  230. self.unitConv = UnitConversion(self)
  231. fromM = 1
  232. if projInfo()['proj'] != 'xy':
  233. fromM = float(projInfo()['meters'])
  234. rectHalfInch = (mapDict['rect'].width/2, mapDict['rect'].height/2)
  235. rectHalfMeter = (self.unitConv.convert(value = rectHalfInch[0], fromUnit = 'inch', toUnit = 'meter')/ fromM /scale,
  236. self.unitConv.convert(value = rectHalfInch[1], fromUnit = 'inch', toUnit = 'meter')/ fromM /scale)
  237. centerE = mapDict['center'][0]
  238. centerN = mapDict['center'][1]
  239. raster = self.instruction.FindInstructionByType('raster')
  240. if raster:
  241. rasterId = raster.id
  242. else:
  243. rasterId = None
  244. if rasterId:
  245. RunCommand('g.region', n = ceil(centerN + rectHalfMeter[1]),
  246. s = floor(centerN - rectHalfMeter[1]),
  247. e = ceil(centerE + rectHalfMeter[0]),
  248. w = floor(centerE - rectHalfMeter[0]),
  249. rast = self.instruction[rasterId]['raster'])
  250. else:
  251. RunCommand('g.region', n = ceil(centerN + rectHalfMeter[1]),
  252. s = floor(centerN - rectHalfMeter[1]),
  253. e = ceil(centerE + rectHalfMeter[0]),
  254. w = floor(centerE - rectHalfMeter[0]))
  255. def projInfo():
  256. """!Return region projection and map units information,
  257. taken from render.py"""
  258. projinfo = dict()
  259. ret = RunCommand('g.proj', read = True, flags = 'p')
  260. if not ret:
  261. return projinfo
  262. for line in ret.splitlines():
  263. if ':' in line:
  264. key, val = line.split(':')
  265. projinfo[key.strip()] = val.strip()
  266. elif "XY location (unprojected)" in line:
  267. projinfo['proj'] = 'xy'
  268. projinfo['units'] = ''
  269. break
  270. return projinfo
  271. def GetMapBounds(filename, portrait = True):
  272. """!Run ps.map -b to get information about map bounding box
  273. @param filename psmap input file
  274. @param portrait page orientation"""
  275. orient = ''
  276. if not portrait:
  277. orient = 'r'
  278. try:
  279. bb = map(float, grass.read_command('ps.map',
  280. flags = 'b' + orient,
  281. quiet = True,
  282. input = filename).strip().split('=')[1].split(','))
  283. except (grass.ScriptError, IndexError):
  284. GError(message = _("Unable to run `ps.map -b`"))
  285. return None
  286. return Rect2D(bb[0], bb[3], bb[2] - bb[0], bb[1] - bb[3])
  287. def getRasterType(map):
  288. """!Returns type of raster map (CELL, FCELL, DCELL)"""
  289. if map is None:
  290. map = ''
  291. file = grass.find_file(name = map, element = 'cell')
  292. if file['file']:
  293. rasterType = grass.raster_info(map)['datatype']
  294. return rasterType
  295. else:
  296. return None
  297. def PilImageToWxImage(pilImage, copyAlpha = True):
  298. """!Convert PIL image to wx.Image
  299. Based on http://wiki.wxpython.org/WorkingWithImages
  300. """
  301. hasAlpha = pilImage.mode[-1] == 'A'
  302. if copyAlpha and hasAlpha : # Make sure there is an alpha layer copy.
  303. wxImage = wx.EmptyImage( *pilImage.size )
  304. pilImageCopyRGBA = pilImage.copy()
  305. pilImageCopyRGB = pilImageCopyRGBA.convert('RGB') # RGBA --> RGB
  306. pilImageRgbData = pilImageCopyRGB.tostring()
  307. wxImage.SetData(pilImageRgbData)
  308. wxImage.SetAlphaData(pilImageCopyRGBA.tostring()[3::4]) # Create layer and insert alpha values.
  309. else : # The resulting image will not have alpha.
  310. wxImage = wx.EmptyImage(*pilImage.size)
  311. pilImageCopy = pilImage.copy()
  312. pilImageCopyRGB = pilImageCopy.convert('RGB') # Discard any alpha from the PIL image.
  313. pilImageRgbData = pilImageCopyRGB.tostring()
  314. wxImage.SetData(pilImageRgbData)
  315. return wxImage
  316. def BBoxAfterRotation(w, h, angle):
  317. """!Compute bounding box or rotated rectangle
  318. @param w rectangle width
  319. @param h rectangle height
  320. @param angle angle (0, 360) in degrees
  321. """
  322. angleRad = angle / 180. * pi
  323. ct = cos(angleRad)
  324. st = sin(angleRad)
  325. hct = h * ct
  326. wct = w * ct
  327. hst = h * st
  328. wst = w * st
  329. y = x = 0
  330. if 0 < angle <= 90:
  331. y_min = y
  332. y_max = y + hct + wst
  333. x_min = x - hst
  334. x_max = x + wct
  335. elif 90 < angle <= 180:
  336. y_min = y + hct
  337. y_max = y + wst
  338. x_min = x - hst + wct
  339. x_max = x
  340. elif 180 < angle <= 270:
  341. y_min = y + wst + hct
  342. y_max = y
  343. x_min = x + wct
  344. x_max = x - hst
  345. elif 270 < angle <= 360:
  346. y_min = y + wst
  347. y_max = y + hct
  348. x_min = x
  349. x_max = x + wct - hst
  350. width = int(ceil(abs(x_max) + abs(x_min)))
  351. height = int(ceil(abs(y_max) + abs(y_min)))
  352. return width, height