d.polar.py 15 KB

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