utils.py 15 KB

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