d.polar.py 15 KB

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