d.polar.py 16 KB

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