utils.py 16 KB

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