d.polar.py 15 KB

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