d.polar.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  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
  26. #% key: map
  27. #% type: string
  28. #% key_desc: name
  29. #% gisprompt: old,cell,raster
  30. #% description: Name of raster angle map
  31. #% required : yes
  32. #%End
  33. #%option
  34. #% key: undef
  35. #% type: double
  36. #% description: Pixel value to be interpreted as undefined (different from NULL)
  37. #% required : no
  38. #%End
  39. #%option
  40. #% key: eps
  41. #% type: string
  42. #% gisprompt: new_file,file,output
  43. #% description: Name of optional EPS output file
  44. #% required : no
  45. #%end
  46. #%flag
  47. #% key: x
  48. #% description: Plot using Xgraph
  49. #%end
  50. import sys
  51. import os
  52. import string
  53. import types
  54. import math
  55. import atexit
  56. import glob
  57. import shutil
  58. from grass.script import core as grass
  59. def cleanup():
  60. grass.try_remove(tmp)
  61. for f in glob.glob(tmp + '_*'):
  62. grass.try_remove(f)
  63. def plot_xgraph():
  64. newline = ['\n']
  65. p = grass.Popen(['xgraph'], stdin = grass.PIPE)
  66. for point in sine_cosine_replic + newline + outercircle + newline + vector:
  67. if isinstance(point, types.TupleType):
  68. p.stdin.write("%f %f\n" % point)
  69. else:
  70. p.stdin.write(point + '\n')
  71. p.stdin.close()
  72. p.wait()
  73. def plot_dgraph():
  74. # use d.info and d.frame to create a square frame in the center of the window.
  75. s = grass.read_command('d.info', flags = 'd')
  76. f = s.split()
  77. frame_width = float(f[1])
  78. frame_height = float(f[2])
  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_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
  96. if isinstance(p, types.TupleType)]
  97. # create circle
  98. circle = [(50*(1 + ring * math.sin(math.radians(i))),
  99. 50*(1 + ring * math.cos(math.radians(i))))
  100. for i in range(0, 361)]
  101. # trend vector
  102. vect = [((scaleval * p[0]/maxradius + 1)*50,
  103. (scaleval * p[1]/maxradius + 1)*50)
  104. for p in vector[1:]]
  105. # Possible TODOs:
  106. # To fill data area with color, use BOTH d.graph's polyline and polygon commands.
  107. # Using polygon alone gives a jagged boundary due to sampling technique (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 3 3",
  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 2",
  144. "color 0:180:0",
  145. "move 1.5 96.5",
  146. "text All data (incl. NULLs)",
  147. "color red",
  148. "move 1.5 93.5",
  149. "text Real data angles",
  150. "color blue",
  151. "move 1.5 90.5",
  152. "text Avg. direction"
  153. ]
  154. p = grass.feed_command('d.graph', env = tenv)
  155. for point in lines:
  156. if isinstance(point, types.TupleType):
  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. grass.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(os.environ['GISBASE'], 'etc', 'd.polar', 'ps_defs.eps')
  202. inf = file(prolog)
  203. shutil.copyfileobj(inf, outf)
  204. inf.close()
  205. t = string.Template("""
  206. $EPSSCALE $EPSSCALE scale %% EPS-SCALE EPS-SCALE scale
  207. %%
  208. %% drawing axes
  209. %%
  210. col0 %% colAXES-COLOR
  211. $AXESLINEWIDTH setlinewidth %% AXES-LINEWIDTH setlinewidth
  212. [] 0 setdash
  213. newpath
  214. $HALFFRAME 0.0 moveto %% HALF-FRAME 0.0 moveto
  215. $HALFFRAME $HALFFRAME_2 lineto %% HALF-FRAME (2 * HALF-FRAME) lineto
  216. 0.0 $HALFFRAME moveto %% 0.0 HALF-FRAME moveto
  217. $HALFFRAME_2 $HALFFRAME lineto %% (2 * HALF-FRAME) HALF-FRAME lineto
  218. stroke
  219. %%
  220. %% drawing outer circle
  221. %%
  222. col2 %% colCIRCLE-COLOR
  223. $DIAGRAMFONTSIZE /Times-Roman choose-font %% DIAGRAM-FONTSIZE /Times-Roman choose-font
  224. $DIAGRAMLINEWIDTH setlinewidth %% DIAGRAM-LINEWIDTH setlinewidth
  225. [] 0 setdash
  226. newpath
  227. %% coordinates of rescaled, translated outer circle follow
  228. %% first point moveto, then lineto
  229. """)
  230. s = t.substitute(
  231. AXESLINEWIDTH = axeslinewidth,
  232. DIAGRAMFONTSIZE = diagramfontsize,
  233. DIAGRAMLINEWIDTH = diagramlinewidth,
  234. EPSSCALE = epsscale,
  235. HALFFRAME = halfframe,
  236. HALFFRAME_2 = halfframe_2
  237. )
  238. outf.write(s)
  239. sublength = len(outercircle) - 2
  240. (x, y) = outercircle[1]
  241. outf.write("%.2f %.2f moveto\n" % (x * scale + halfframe, y * scale + halfframe))
  242. for x, y in outercircle[2:]:
  243. outf.write("%.2f %.2f lineto\n" % (x * scale + halfframe, y * scale + halfframe))
  244. t = string.Template("""
  245. stroke
  246. %%
  247. %% axis titles
  248. %%
  249. col0 %% colAXES-COLOR
  250. $AXESFONTSIZE /Times-Roman choose-font %% AXES-FONTSIZE /Times-Roman choose-font
  251. (N) $NORTHXSHIFT $NORTHYSHIFT $NORTHJUSTIFICATION just-string %% NORTH-X-SHIFT NORTH-Y-SHIFT NORTH-JUSTIFICATION just-string
  252. (E) $EASTXSHIFT $EASTYSHIFT $EASTJUSTIFICATION just-string %% EAST-X-SHIFT EAST-Y-SHIFT EAST-JUSTIFICATION just-string
  253. (S) $SOUTHXSHIFT $SOUTHYSHIFT $SOUTHJUSTIFICATION just-string %% SOUTH-X-SHIFT SOUTH-Y-SHIFT SOUTH-JUSTIFICATION just-string
  254. (W) $WESTXSHIFT $WESTYSHIFT $WESTJUSTIFICATION just-string %% WEST-X-SHIFT WEST-Y-SHIFT WEST-JUSTIFICATION just-string
  255. $DIAGRAMFONTSIZE /Times-Roman choose-font %% DIAGRAM-FONTSIZE /Times-Roman choose-font
  256. %%
  257. %% drawing real data diagram
  258. %%
  259. col4 %% colDIAGRAM-COLOR
  260. $DIAGRAMLINEWIDTH setlinewidth %% DIAGRAM-LINEWIDTH setlinewidth
  261. [] 0 setdash
  262. newpath
  263. %% coordinates of rescaled, translated diagram follow
  264. %% first point moveto, then lineto
  265. """)
  266. s = t.substitute(
  267. AXESFONTSIZE = axesfontsize,
  268. DIAGRAMFONTSIZE = diagramfontsize,
  269. DIAGRAMLINEWIDTH = diagramlinewidth,
  270. EASTJUSTIFICATION = eastjustification,
  271. EASTXSHIFT = eastxshift,
  272. EASTYSHIFT = eastyshift,
  273. NORTHJUSTIFICATION = northjustification,
  274. NORTHXSHIFT = northxshift,
  275. NORTHYSHIFT = northyshift,
  276. SOUTHJUSTIFICATION = southjustification,
  277. SOUTHXSHIFT = southxshift,
  278. SOUTHYSHIFT = southyshift,
  279. WESTJUSTIFICATION = westjustification,
  280. WESTXSHIFT = westxshift,
  281. WESTYSHIFT = westyshift
  282. )
  283. outf.write(s)
  284. sublength = len(sine_cosine_replic) - 2
  285. (x, y) = sine_cosine_replic[1]
  286. outf.write("%.2f %.2f moveto\n" % (x * scale + halfframe, y * scale + halfframe))
  287. for x, y in sine_cosine_replic[2:]:
  288. outf.write("%.2f %.2f lineto\n" % (x * scale + halfframe, y * scale + halfframe))
  289. t = string.Template("""
  290. stroke
  291. %%
  292. %% drawing average direction
  293. %%
  294. col1 %% colAVERAGE-DIRECTION-COLOR
  295. $DIAGRAMLINEWIDTH setlinewidth %% DIAGRAM-LINEWIDTH setlinewidth
  296. [] 0 setdash
  297. newpath
  298. %% coordinates of rescaled, translated average direction follow
  299. %% first point moveto, second lineto
  300. """)
  301. s = t.substitute(DIAGRAMLINEWIDTH = diagramlinewidth)
  302. outf.write(s)
  303. sublength = len(vector) - 2
  304. (x, y) = vector[1]
  305. outf.write("%.2f %.2f moveto\n" % (x * scale + halfframe, y * scale + halfframe))
  306. for x, y in vector[2:]:
  307. outf.write("%.2f %.2f lineto\n" % (x * scale + halfframe, y * scale + halfframe))
  308. t = string.Template("""
  309. stroke
  310. %%
  311. %% drawing legends
  312. %%
  313. col2 %% colCIRCLE-COLOR
  314. %% Line below: (ALL-DATA-STRING) LEGENDS-X ALL-DATA-LEGEND-Y 4 just-string
  315. ($ALLDATASTRING) $LEGENDSX $ALLDATALEGENDY 4 just-string
  316. col4 %% colDIAGRAM-COLOR
  317. %% Line below: (REAL-DATA-STRING) LEGENDS-X REAL-DATA-LEGEND-Y 4 just-string
  318. ($REALDATASTRING) $LEGENDSX $REALDATALEGENDY 4 just-string
  319. col1 %% colAVERAGE-DIRECTION-COLOR
  320. %% Line below: (AVERAGE-DIRECTION-STRING) LEGENDS-X AVERAGE-DIRECTION-LEGEND-Y 4 just-string
  321. ($AVERAGEDIRECTIONSTRING) $LEGENDSX $AVERAGEDIRECTIONLEGENDY 4 just-string
  322. """)
  323. s = t.substitute(
  324. ALLDATALEGENDY = alldatalegendy,
  325. ALLDATASTRING = alldatastring,
  326. AVERAGEDIRECTIONLEGENDY = averagedirectionlegendy,
  327. AVERAGEDIRECTIONSTRING = averagedirectionstring,
  328. LEGENDSX = legendsx,
  329. REALDATALEGENDY = realdatalegendy,
  330. REALDATASTRING = realdatastring
  331. )
  332. outf.write(s)
  333. outf.close()
  334. grass.message(_("Done."))
  335. def main():
  336. global tmp
  337. global sine_cosine_replic, outercircle, vector
  338. global totalvalidnumber, totalnumber, maxradius
  339. map = options['map']
  340. undef = options['undef']
  341. eps = options['eps']
  342. xgraph = flags['x']
  343. tmp = grass.tempfile()
  344. if eps and xgraph:
  345. grass.fatal(_("Please select only one output method"))
  346. #### check if we have xgraph (if no EPS output requested)
  347. if xgraph and not grass.find_program('xgraph'):
  348. grass.fatal(_("xgraph required, please install first (www.xgraph.org)"))
  349. #################################
  350. # this file contains everthing:
  351. rawfile = tmp + "_raw"
  352. rawf = file(rawfile, 'w')
  353. grass.run_command('r.stats', flags = '1', input = map, stdout = rawf)
  354. rawf.close()
  355. rawf = file(rawfile)
  356. totalnumber = 0
  357. for line in rawf:
  358. totalnumber += 1
  359. rawf.close()
  360. grass.message(_("Calculating statistics for polar diagram... (be patient)"))
  361. #wipe out NULL data and undef data if defined by user
  362. # - generate degree binned to integer, eliminate NO DATA (NULL):
  363. # change 360 to 0 to close polar diagram:
  364. rawf = file(rawfile)
  365. nvals = 0
  366. sumcos = 0
  367. sumsin = 0
  368. freq = {}
  369. for line in rawf:
  370. line = line.rstrip('\r\n')
  371. if line in ['*', undef]:
  372. continue
  373. nvals += 1
  374. x = float(line)
  375. rx = math.radians(x)
  376. sumcos += math.cos(rx)
  377. sumsin += math.sin(rx)
  378. ix = round(x)
  379. if ix == 360:
  380. ix = 0
  381. if ix in freq:
  382. freq[ix] += 1
  383. else:
  384. freq[ix] = 1
  385. rawf.close()
  386. totalvalidnumber = nvals
  387. if totalvalidnumber == 0:
  388. grass.fatal(_("No data pixel found"))
  389. #################################
  390. # unit vector on raw data converted to radians without no data:
  391. unitvector = (sumcos / nvals, sumsin / nvals)
  392. #################################
  393. # how many are there?:
  394. occurrences = [(math.radians(x), freq[x]) for x in freq]
  395. occurrences.sort()
  396. # find the maximum value
  397. maxradius = max([f for a, f in occurrences])
  398. # now do cos() sin()
  399. sine_cosine = [(math.cos(a) * f, math.sin(a) * f) for a, f in occurrences]
  400. sine_cosine_replic = ['"Real data angles'] + sine_cosine + sine_cosine[0:1]
  401. if eps or xgraph:
  402. outercircle = []
  403. outercircle.append('"All Data incl. NULLs')
  404. scale = 1.0 * totalnumber / totalvalidnumber * maxradius
  405. for i in range(0, 361):
  406. a = math.radians(i)
  407. x = math.cos(a) * scale
  408. y = math.sin(a) * scale
  409. outercircle.append((x, y))
  410. # fix vector length to become visible (x? of $MAXRADIUS):
  411. vector = []
  412. vector.append('"Avg. Direction\n')
  413. vector.append((0, 0))
  414. vector.append((unitvector[0] * maxradius, unitvector[1] * maxradius))
  415. ###########################################################
  416. # Now output:
  417. if eps:
  418. psout = grass.basename(eps, 'eps') + '.eps'
  419. plot_eps(psout)
  420. elif xgraph:
  421. plot_xgraph()
  422. else:
  423. plot_dgraph()
  424. grass.message(_("Average vector:"))
  425. grass.message(_("direction: %.1f degrees CCW from East") % math.degrees(math.atan2(unitvector[1], unitvector[0])))
  426. grass.message(_("magnitude: %.1f percent of fullscale") % (100 * math.hypot(unitvector[0], unitvector[1])))
  427. if __name__ == "__main__":
  428. options, flags = grass.parser()
  429. atexit.register(cleanup)
  430. main()