d.polar.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. #!/usr/bin/env python
  2. #
  3. ############################################################################
  4. #
  5. # MODULE: d.polar
  6. # AUTHOR(S): Markus Neteler. neteler itc.it
  7. # algorithm + EPS output by Bruno Caprile
  8. # d.graph plotting code by Hamish Bowman
  9. # Converted to Python by Glynn Clements
  10. # PURPOSE: Draws polar diagram of angle map. The outer circle considers
  11. # all cells in the map. If one or many of them are NULL (no data),
  12. # the figure will not reach the outer circle. The vector inside
  13. # indicates the prevalent direction.
  14. # COPYRIGHT: (C) 2006,2008 by the GRASS Development Team
  15. #
  16. # This program is free software under the GNU General Public
  17. # License (>=v2). Read the file COPYING that comes with GRASS
  18. # for details.
  19. #
  20. #############################################################################
  21. #%Module
  22. #% description: Draws polar diagram of angle map such as aspect or flow directions
  23. #% keyword: display
  24. #% keyword: diagram
  25. #%End
  26. #%option G_OPT_R_MAP
  27. #% description: Name of raster angle map
  28. #%End
  29. #%option
  30. #% key: undef
  31. #% type: double
  32. #% description: Pixel value to be interpreted as undefined (different from NULL)
  33. #% required : no
  34. #%End
  35. #%option G_OPT_F_OUTPUT
  36. #% description: Name for optional EPS output file
  37. #% required : no
  38. #%end
  39. #%flag
  40. #% key: x
  41. #% description: Plot using Xgraph
  42. #%end
  43. import os
  44. import string
  45. import types
  46. import math
  47. import atexit
  48. import glob
  49. import shutil
  50. from grass.script.utils import try_remove, basename
  51. from grass.script import core as gcore
  52. # i18N
  53. import gettext
  54. gettext.install('grassmods', os.path.join(os.getenv("GISBASE"), 'locale'))
  55. def raster_map_required(name):
  56. if not gcore.find_file(name, 'cell')['file']:
  57. gcore.fatal(_("Raster map <%s> not found") % name)
  58. def cleanup():
  59. try_remove(tmp)
  60. for f in glob.glob(tmp + '_*'):
  61. try_remove(f)
  62. def plot_xgraph():
  63. newline = ['\n']
  64. p = gcore.Popen(['xgraph'], stdin=gcore.PIPE)
  65. for point in sine_cosine_replic + newline + outercircle + newline + vector:
  66. if isinstance(point, tuple):
  67. p.stdin.write("%f %f\n" % point)
  68. else:
  69. p.stdin.write(point + '\n')
  70. p.stdin.close()
  71. p.wait()
  72. def plot_dgraph():
  73. # use d.info and d.frame to create a square frame in the center of the
  74. # window.
  75. s = gcore.read_command('d.info', flags='d')
  76. f = s.split()
  77. frame_width = float(f[2])
  78. frame_height = float(f[3])
  79. # take shorter side as length of frame side
  80. min_side = min(frame_width, frame_height)
  81. dx = (frame_width - min_side) / 2
  82. dy = (frame_height - min_side) / 2
  83. fl = dx
  84. fr = frame_width - dx
  85. ft = dy
  86. fb = frame_height - dy
  87. tenv = os.environ.copy()
  88. tenv['GRASS_RENDER_FRAME'] = '%f,%f,%f,%f' % (ft, fb, fl, fr)
  89. # polyline calculations
  90. ring = 0.95
  91. scaleval = ring * totalvalidnumber / totalnumber
  92. sine_cosine_replic_normalized = [
  93. ((scaleval * p[0] / maxradius + 1) * 50,
  94. (scaleval * p[1] / maxradius + 1) * 50)
  95. for p in sine_cosine_replic if isinstance(p, tuple)]
  96. # create circle
  97. circle = [(50 * (1 + ring * math.sin(math.radians(i))),
  98. 50 * (1 + ring * math.cos(math.radians(i))))
  99. for i in range(0, 361)]
  100. # trend vector
  101. vect = [((scaleval * p[0] / maxradius + 1) * 50,
  102. (scaleval * p[1] / maxradius + 1) * 50)
  103. for p in vector[1:]]
  104. # Possible TODOs:
  105. # To fill data area with color, use BOTH d.graph's polyline and polygon commands.
  106. # Using polygon alone gives a jagged boundary due to sampling technique
  107. # (not a bug).
  108. # plot it!
  109. lines = [
  110. # draw circle
  111. # mandatory when drawing proportional to non-square frame
  112. "color 180:255:180",
  113. "polyline"] + circle + [
  114. # draw axes
  115. "color 180:180:180",
  116. "width 0",
  117. "move 0 50",
  118. "draw 100 50",
  119. "move 50 0",
  120. "draw 50 100",
  121. # draw the goods
  122. "color red",
  123. "width 0",
  124. "polyline"] + sine_cosine_replic_normalized + [
  125. # draw vector
  126. "color blue",
  127. "width 3",
  128. "polyline"] + vect + [
  129. # draw compass text
  130. "color black",
  131. "width 2",
  132. "size 10 10",
  133. "move 51 97",
  134. "text N",
  135. "move 51 1",
  136. "text S",
  137. "move 1 51",
  138. "text W",
  139. "move 97 51",
  140. "text E",
  141. # draw legend text
  142. "width 0",
  143. "size 10",
  144. "color 0:180:0",
  145. "move 0.5 96.5",
  146. "text All data (incl. NULLs)",
  147. "color red",
  148. "move 0.5 93.5",
  149. "text Real data angles",
  150. "color blue",
  151. "move 0.5 90.5",
  152. "text Avg. direction"
  153. ]
  154. p = gcore.feed_command('d.graph', env=tenv)
  155. for point in lines:
  156. if isinstance(point, tuple):
  157. p.stdin.write("%f %f\n" % point)
  158. else:
  159. p.stdin.write(point + '\n')
  160. p.stdin.close()
  161. p.wait()
  162. def plot_eps(psout):
  163. # EPS output (by M.Neteler and Bruno Caprile, ITC-irst)
  164. gcore.message(_("Generating %s ...") % psout)
  165. outerradius = maxradius
  166. epsscale = 0.1
  167. frameallowance = 1.1
  168. halfframe = 3000
  169. center = (halfframe, halfframe)
  170. scale = halfframe / (outerradius * frameallowance)
  171. diagramlinewidth = halfframe / 400
  172. axeslinewidth = halfframe / 500
  173. axesfontsize = halfframe / 16
  174. diagramfontsize = halfframe / 20
  175. halfframe_2 = halfframe * 2
  176. averagedirectioncolor = 1 # (blue)
  177. diagramcolor = 4 # (red)
  178. circlecolor = 2 # (green)
  179. axescolor = 0 # (black)
  180. northjustification = 2
  181. eastjustification = 6
  182. southjustification = 8
  183. westjustification = 8
  184. northxshift = 1.02 * halfframe
  185. northyshift = 1.98 * halfframe
  186. eastxshift = 1.98 * halfframe
  187. eastyshift = 1.02 * halfframe
  188. southxshift = 1.02 * halfframe
  189. southyshift = 0.02 * halfframe
  190. westxshift = 0.01 * halfframe
  191. westyshift = 1.02 * halfframe
  192. alldatastring = "all data (null included)"
  193. realdatastring = "real data angles"
  194. averagedirectionstring = "avg. direction"
  195. legendsx = 1.95 * halfframe
  196. alldatalegendy = 1.95 * halfframe
  197. realdatalegendy = 1.90 * halfframe
  198. averagedirectionlegendy = 1.85 * halfframe
  199. ##########
  200. outf = file(psout, 'w')
  201. prolog = os.path.join(
  202. os.environ['GISBASE'],
  203. 'etc',
  204. 'd.polar',
  205. 'ps_defs.eps')
  206. inf = file(prolog)
  207. shutil.copyfileobj(inf, outf)
  208. inf.close()
  209. t = string.Template("""
  210. $EPSSCALE $EPSSCALE scale %% EPS-SCALE EPS-SCALE scale
  211. %%
  212. %% drawing axes
  213. %%
  214. col0 %% colAXES-COLOR
  215. $AXESLINEWIDTH setlinewidth %% AXES-LINEWIDTH setlinewidth
  216. [] 0 setdash
  217. newpath
  218. $HALFFRAME 0.0 moveto %% HALF-FRAME 0.0 moveto
  219. $HALFFRAME $HALFFRAME_2 lineto %% HALF-FRAME (2 * HALF-FRAME) lineto
  220. 0.0 $HALFFRAME moveto %% 0.0 HALF-FRAME moveto
  221. $HALFFRAME_2 $HALFFRAME lineto %% (2 * HALF-FRAME) HALF-FRAME lineto
  222. stroke
  223. %%
  224. %% drawing outer circle
  225. %%
  226. col2 %% colCIRCLE-COLOR
  227. $DIAGRAMFONTSIZE /Times-Roman choose-font %% DIAGRAM-FONTSIZE /Times-Roman choose-font
  228. $DIAGRAMLINEWIDTH setlinewidth %% DIAGRAM-LINEWIDTH setlinewidth
  229. [] 0 setdash
  230. newpath
  231. %% coordinates of rescaled, translated outer circle follow
  232. %% first point moveto, then lineto
  233. """)
  234. s = t.substitute(
  235. AXESLINEWIDTH=axeslinewidth,
  236. DIAGRAMFONTSIZE=diagramfontsize,
  237. DIAGRAMLINEWIDTH=diagramlinewidth,
  238. EPSSCALE=epsscale,
  239. HALFFRAME=halfframe,
  240. HALFFRAME_2=halfframe_2
  241. )
  242. outf.write(s)
  243. sublength = len(outercircle) - 2
  244. (x, y) = outercircle[1]
  245. outf.write(
  246. "%.2f %.2f moveto\n" %
  247. (x * scale + halfframe, y * scale + halfframe))
  248. for x, y in outercircle[2:]:
  249. outf.write(
  250. "%.2f %.2f lineto\n" %
  251. (x * scale + halfframe, y * scale + halfframe))
  252. t = string.Template("""
  253. stroke
  254. %%
  255. %% axis titles
  256. %%
  257. col0 %% colAXES-COLOR
  258. $AXESFONTSIZE /Times-Roman choose-font %% AXES-FONTSIZE /Times-Roman choose-font
  259. (N) $NORTHXSHIFT $NORTHYSHIFT $NORTHJUSTIFICATION just-string %% NORTH-X-SHIFT NORTH-Y-SHIFT NORTH-JUSTIFICATION just-string
  260. (E) $EASTXSHIFT $EASTYSHIFT $EASTJUSTIFICATION just-string %% EAST-X-SHIFT EAST-Y-SHIFT EAST-JUSTIFICATION just-string
  261. (S) $SOUTHXSHIFT $SOUTHYSHIFT $SOUTHJUSTIFICATION just-string %% SOUTH-X-SHIFT SOUTH-Y-SHIFT SOUTH-JUSTIFICATION just-string
  262. (W) $WESTXSHIFT $WESTYSHIFT $WESTJUSTIFICATION just-string %% WEST-X-SHIFT WEST-Y-SHIFT WEST-JUSTIFICATION just-string
  263. $DIAGRAMFONTSIZE /Times-Roman choose-font %% DIAGRAM-FONTSIZE /Times-Roman choose-font
  264. %%
  265. %% drawing real data diagram
  266. %%
  267. col4 %% colDIAGRAM-COLOR
  268. $DIAGRAMLINEWIDTH setlinewidth %% DIAGRAM-LINEWIDTH setlinewidth
  269. [] 0 setdash
  270. newpath
  271. %% coordinates of rescaled, translated diagram follow
  272. %% first point moveto, then lineto
  273. """)
  274. s = t.substitute(
  275. AXESFONTSIZE=axesfontsize,
  276. DIAGRAMFONTSIZE=diagramfontsize,
  277. DIAGRAMLINEWIDTH=diagramlinewidth,
  278. EASTJUSTIFICATION=eastjustification,
  279. EASTXSHIFT=eastxshift,
  280. EASTYSHIFT=eastyshift,
  281. NORTHJUSTIFICATION=northjustification,
  282. NORTHXSHIFT=northxshift,
  283. NORTHYSHIFT=northyshift,
  284. SOUTHJUSTIFICATION=southjustification,
  285. SOUTHXSHIFT=southxshift,
  286. SOUTHYSHIFT=southyshift,
  287. WESTJUSTIFICATION=westjustification,
  288. WESTXSHIFT=westxshift,
  289. WESTYSHIFT=westyshift
  290. )
  291. outf.write(s)
  292. sublength = len(sine_cosine_replic) - 2
  293. (x, y) = sine_cosine_replic[1]
  294. outf.write(
  295. "%.2f %.2f moveto\n" %
  296. (x * scale + halfframe, y * scale + halfframe))
  297. for x, y in sine_cosine_replic[2:]:
  298. outf.write(
  299. "%.2f %.2f lineto\n" %
  300. (x * scale + halfframe, y * scale + halfframe))
  301. t = string.Template("""
  302. stroke
  303. %%
  304. %% drawing average direction
  305. %%
  306. col1 %% colAVERAGE-DIRECTION-COLOR
  307. $DIAGRAMLINEWIDTH setlinewidth %% DIAGRAM-LINEWIDTH setlinewidth
  308. [] 0 setdash
  309. newpath
  310. %% coordinates of rescaled, translated average direction follow
  311. %% first point moveto, second lineto
  312. """)
  313. s = t.substitute(DIAGRAMLINEWIDTH=diagramlinewidth)
  314. outf.write(s)
  315. sublength = len(vector) - 2
  316. (x, y) = vector[1]
  317. outf.write(
  318. "%.2f %.2f moveto\n" %
  319. (x * scale + halfframe, y * scale + halfframe))
  320. for x, y in vector[2:]:
  321. outf.write(
  322. "%.2f %.2f lineto\n" %
  323. (x * scale + halfframe, y * scale + halfframe))
  324. t = string.Template("""
  325. stroke
  326. %%
  327. %% drawing legends
  328. %%
  329. col2 %% colCIRCLE-COLOR
  330. %% Line below: (ALL-DATA-STRING) LEGENDS-X ALL-DATA-LEGEND-Y 4 just-string
  331. ($ALLDATASTRING) $LEGENDSX $ALLDATALEGENDY 4 just-string
  332. col4 %% colDIAGRAM-COLOR
  333. %% Line below: (REAL-DATA-STRING) LEGENDS-X REAL-DATA-LEGEND-Y 4 just-string
  334. ($REALDATASTRING) $LEGENDSX $REALDATALEGENDY 4 just-string
  335. col1 %% colAVERAGE-DIRECTION-COLOR
  336. %% Line below: (AVERAGE-DIRECTION-STRING) LEGENDS-X AVERAGE-DIRECTION-LEGEND-Y 4 just-string
  337. ($AVERAGEDIRECTIONSTRING) $LEGENDSX $AVERAGEDIRECTIONLEGENDY 4 just-string
  338. """)
  339. s = t.substitute(
  340. ALLDATALEGENDY=alldatalegendy,
  341. ALLDATASTRING=alldatastring,
  342. AVERAGEDIRECTIONLEGENDY=averagedirectionlegendy,
  343. AVERAGEDIRECTIONSTRING=averagedirectionstring,
  344. LEGENDSX=legendsx,
  345. REALDATALEGENDY=realdatalegendy,
  346. REALDATASTRING=realdatastring
  347. )
  348. outf.write(s)
  349. outf.close()
  350. gcore.message(_("Done."))
  351. def main():
  352. global tmp
  353. global sine_cosine_replic, outercircle, vector
  354. global totalvalidnumber, totalnumber, maxradius
  355. map = options['map']
  356. undef = options['undef']
  357. eps = options['output']
  358. xgraph = flags['x']
  359. tmp = gcore.tempfile()
  360. if eps and xgraph:
  361. gcore.fatal(_("Please select only one output method"))
  362. # check if we have xgraph (if no EPS output requested)
  363. if xgraph and not gcore.find_program('xgraph'):
  364. gcore.fatal(
  365. _("xgraph required, please install first (www.xgraph.org)"))
  366. raster_map_required(map)
  367. #################################
  368. # this file contains everything:
  369. rawfile = tmp + "_raw"
  370. rawf = file(rawfile, 'w')
  371. gcore.run_command('r.stats', flags='1', input=map, stdout=rawf)
  372. rawf.close()
  373. rawf = file(rawfile)
  374. totalnumber = 0
  375. for line in rawf:
  376. totalnumber += 1
  377. rawf.close()
  378. gcore.message(
  379. _("Calculating statistics for polar diagram... (be patient)"))
  380. # wipe out NULL data and undef data if defined by user
  381. # - generate degree binned to integer, eliminate NO DATA (NULL):
  382. # change 360 to 0 to close polar diagram:
  383. rawf = file(rawfile)
  384. nvals = 0
  385. sumcos = 0
  386. sumsin = 0
  387. freq = {}
  388. for line in rawf:
  389. line = line.rstrip('\r\n')
  390. if line in ['*', undef]:
  391. continue
  392. nvals += 1
  393. x = float(line)
  394. rx = math.radians(x)
  395. sumcos += math.cos(rx)
  396. sumsin += math.sin(rx)
  397. ix = round(x)
  398. if ix == 360:
  399. ix = 0
  400. if ix in freq:
  401. freq[ix] += 1
  402. else:
  403. freq[ix] = 1
  404. rawf.close()
  405. totalvalidnumber = nvals
  406. if totalvalidnumber == 0:
  407. gcore.fatal(_("No data pixel found"))
  408. #################################
  409. # unit vector on raw data converted to radians without no data:
  410. unitvector = (sumcos / nvals, sumsin / nvals)
  411. #################################
  412. # how many are there?:
  413. occurrences = sorted([(math.radians(x), freq[x]) for x in freq])
  414. # find the maximum value
  415. maxradius = max([f for a, f in occurrences])
  416. # now do cos() sin()
  417. sine_cosine = [(math.cos(a) * f, math.sin(a) * f) for a, f in occurrences]
  418. sine_cosine_replic = ['"Real data angles'] + sine_cosine + sine_cosine[0:1]
  419. if eps or xgraph:
  420. outercircle = []
  421. outercircle.append('"All Data incl. NULLs')
  422. scale = 1.0 * totalnumber / totalvalidnumber * maxradius
  423. for i in range(0, 361):
  424. a = math.radians(i)
  425. x = math.cos(a) * scale
  426. y = math.sin(a) * scale
  427. outercircle.append((x, y))
  428. # fix vector length to become visible (x? of $MAXRADIUS):
  429. vector = []
  430. vector.append('"Avg. Direction\n')
  431. vector.append((0, 0))
  432. vector.append((unitvector[0] * maxradius, unitvector[1] * maxradius))
  433. ###########################################################
  434. # Now output:
  435. if eps:
  436. psout = basename(eps, 'eps') + '.eps'
  437. plot_eps(psout)
  438. elif xgraph:
  439. plot_xgraph()
  440. else:
  441. plot_dgraph()
  442. gcore.message(_("Average vector:"))
  443. gcore.message(
  444. _("direction: %.1f degrees CCW from East") %
  445. math.degrees(
  446. math.atan2(
  447. unitvector[1],
  448. unitvector[0])))
  449. gcore.message(
  450. _("magnitude: %.1f percent of fullscale") %
  451. (100 *
  452. math.hypot(
  453. unitvector[0],
  454. unitvector[1])))
  455. if __name__ == "__main__":
  456. options, flags = gcore.parser()
  457. atexit.register(cleanup)
  458. main()