grassgis 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 31603b3573329e791fb150b29bdbc2431ac67b73
4
- data.tar.gz: 22cef2943a628277b2811565625be816400a5cfa
3
+ metadata.gz: cf52083bd1c17e88f35637c33190bb5118c315ae
4
+ data.tar.gz: d839e49ddad296eaf6571b7c84accc7afeb91222
5
5
  SHA512:
6
- metadata.gz: b5f0bc0d8e6f86f97e25ffb8d70ad2ba7dc34b699be10350e0f72ffd67162760ff08c0fa5477ecd63829386e3c3f4d82c2d248ab2aa9972880ca1b26346e54b6
7
- data.tar.gz: 9f652b3f652615c98ab606fee366546d6c4cac5ca5c47e69b87eed9479496c9f9bc9110ec94e435b7fd1295a20aa68fb9170f655bcc9e032f23bae571a73301b
6
+ metadata.gz: bd781b3d5cbb21cd66512b72aa8146beed1f7488c7683ecee4c3af8e0988de0939e660cfe9658051f6a01c71694deb521fa1c3247f89de6d8381e28ba5930a09
7
+ data.tar.gz: 54d0b9056cee6f5382bdbc3dfc690bd6b2aa79a4777c88ef00dc153cc6b20e2d0bb10676e1253361e200371829ca5850f76e23315f57676f4fea7465f8000698
data/.gitignore CHANGED
@@ -1,22 +1,22 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
18
- *.bundle
19
- *.so
20
- *.o
21
- *.a
22
- mkmf.log
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
@@ -1,7 +1,7 @@
1
- language: ruby
2
- rvm:
3
- - 2.2.2
4
- - 2.1.6
5
- - 1.9.3
6
- before_install:
7
- - gem update bundler
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ - 2.1.6
5
+ - 1.9.3
6
+ before_install:
7
+ - gem update bundler
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in grassgis.gemspec
4
- gemspec
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in grassgis.gemspec
4
+ gemspec
@@ -1,22 +1,22 @@
1
- Copyright (c) 2015 Javier Goizueta
2
-
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ Copyright (c) 2015 Javier Goizueta
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,578 +1,722 @@
1
- # GrassGis
2
-
3
- [![Gem Version](https://badge.fury.io/rb/grassgis.svg)](http://badge.fury.io/rb/grassgis)
4
- [![Build Status](https://travis-ci.org/jgoizueta/grassgis.svg)](https://travis-ci.org/jgoizueta/grassgis)
5
-
6
- Support for scripting GRASS with Ruby.
7
-
8
- <!-- START doctoc generated TOC please keep comment here to allow auto update -->
9
- <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
10
- **Table of Contents**
11
-
12
- - [Installation](#installation)
13
- - [Usage](#usage)
14
- - [Configuration](#configuration)
15
- - [Running a GRASS Session](#running-a-grass-session)
16
- - [Creating new locations and mapsets](#creating-new-locations-and-mapsets)
17
- - [History](#history)
18
- - [Options](#options)
19
- - [Echo](#echo)
20
- - [Errors](#errors)
21
- - [Logging](#logging)
22
- - [Technicalities](#technicalities)
23
- - [Session scopes](#session-scopes)
24
- - [Invalid commands](#invalid-commands)
25
- - [Helper methods](#helper-methods)
26
- - [Examples](#examples)
27
- - [1. Map existence](#1-map-existence)
28
- - [2. Information as Hashes](#2-information-as-hashes)
29
- - [3. Average angle](#3-average-angle)
30
- - [Roadmap](#roadmap)
31
- - [GRASS cooking DSL](#grass-cooking-dsl)
32
- - [Contributing](#contributing)
33
-
34
- <!-- END doctoc generated TOC please keep comment here to allow auto update -->
35
-
36
- ## Installation
37
-
38
- Add this line to your application's Gemfile:
39
-
40
- gem 'grassgis'
41
-
42
- And then execute:
43
-
44
- $ bundle
45
-
46
- Or install it yourself as:
47
-
48
- $ gem install grassgis
49
-
50
- ## Usage
51
-
52
- This library can prepare environments to execute GRASS commands
53
- from Ruby scripts.
54
-
55
- First we require the library:
56
-
57
- ```ruby
58
- require 'grassgis'
59
- ```
60
-
61
- ### Configuration
62
-
63
- A GRASS session operates on a given location and mapset.
64
-
65
- Before starting a GRASS session we need some configuration
66
- parameters to specify the location of the GRASS Installation
67
- to be used and the location/mapset. We do this by using
68
- a Ruby Hash containing configuration parameters:
69
-
70
- ```ruby
71
- configuration = {
72
- gisbase: '/usr/local/grass-7.0.0',
73
- gisdbase: File.join(ENV['HOME'], 'grassdata'),
74
- location: 'nc_spm',
75
- mapset: 'user1'
76
- }
77
- ```
78
-
79
- So, you first need to know where is GRASS installed on your system
80
- to define the `:gisbase` option to point to the base directory of the GRASS
81
- installation.
82
-
83
- For *Ubuntu* it will typically be `/usr/lib/grass70` for version 7 of GRASS.
84
-
85
- In *Windows*, if installed with
86
- [OSGeo4W](http://trac.osgeo.org/osgeo4w/) it is typically of
87
- the form `C:\OGeo4W\app\grass\grass-7.0.0` (the last directory will vary
88
- depending on the GRASS version).
89
-
90
- Under *Mac OS X*, if using [Homebrew](http://brew.sh/)
91
- (with the [osgeo/osgeo4mac](https://github.com/OSGeo/homebrew-osgeo4mac) tap)
92
- it should bd something like `/usr/local/Cellar/grass-70/7.0.0/grass-7.0.0`.
93
-
94
- You can find the `:gisbase` directory by executing the
95
- `grass` command of your system (which may be `grass70`, `grass64`, etc.)
96
- with the `--config path` option:
97
-
98
- grass --config path
99
-
100
- You must also specify the `GISDBASE`, `LOCATION` and `MAPSET`, to work with,
101
- just like when starting GRASS, through the `:gisdbase`, `:location` and
102
- `:mapset` configuration options.
103
- You can omit `:gisdbase` which will default to a directory named `grassdata` in the
104
- user's home directory and `:mapset` which defaults to `PERMANENT`.
105
-
106
-
107
- ### Running a GRASS Session
108
-
109
- With the proper configuration in place, we can use it to
110
- create a GRASS Session and execute GRASS command from it:
111
-
112
- ```ruby
113
- GrassGis.session configuration do
114
- g.list 'vect'
115
- puts output # will print list of vector maps
116
- end
117
- ```
118
-
119
- Inside a `GrassGis` session we can execute GRASS commands
120
- just by using the command name as a Ruby method.
121
-
122
- Command flags and options must be passed first as regular method arguments,
123
- then named parameters must be passed as a Ruby Hash:
124
-
125
- ```ruby
126
- g.region '-p', rast: 'elevation'
127
- d.rast 'elevation'
128
- d.vect 'streams', col: 'blue'
129
- ```
130
-
131
- If you try to execute an invalid module name an `ENOENT`
132
- error will be raised:
133
-
134
- ```ruby
135
- g.this.module.does.not.exist '???'
136
- ```
137
-
138
- If the command to be executed has no arguments you need
139
- to invoke `.run` on it to execute it:
140
-
141
- ```ruby
142
- d.erase.run
143
- g.list.run
144
- ```
145
-
146
- ### Creating new locations and mapsets
147
-
148
- To create a new location and/or mapset, open a session to it
149
- and use a `:create` parameter like this:
150
-
151
- ```ruby
152
- options = configuration.merge(
153
- location: 'new_location',
154
- mapset: 'new_mapset',
155
- create: {
156
- epsg: 4326 # coordinate system for the new location
157
- limits: [-5, 30, 5, 50], # optional E, S, W, N limits
158
- res: 2 # optional resolution
159
- }
160
- )
161
- GrassGis.session options do
162
- g.region '-p'
163
- puts output
164
- end
165
- ```
166
-
167
- Use `nil` or `PERMANENT` for the mapset to avoid creating a new mapset.
168
-
169
- Existing locations or mapsets are not changed.
170
-
171
- ### History
172
-
173
- The return value of a GRASS command invocation inside a session is
174
- a `SysCmd::Command`
175
- (see the [sys_cmd gem](https://github.com/jgoizueta/sys_cmd)).
176
-
177
- ```ruby
178
- GrassGis.session configuration do+
179
- cmd = g.region '-p'
180
- puts cmd.output # command output is kept in the Command object
181
- puts cmd.status_value # 0 for success
182
- end
183
- ```
184
-
185
- You don't need to assign commands to variables as in the example
186
- to access them, because they're all kept in an array accesible
187
- through the `history` method of the session:
188
-
189
- ```ruby
190
- GrassGis.session configuration do+
191
- r.info 'slope'
192
- g.region '-p'
193
- puts history.size # 2
194
- puts history[-1].output # output of g.region
195
- puts history[-2].output # output of r.info
196
- end
197
- ```
198
-
199
- The last executed command (`history[-1]`) is also accessible through
200
- the `last` method and its output through `output`:
201
-
202
- ```ruby
203
- GrassGis.session configuration do+
204
- r.info 'slope'
205
- g.region '-p'
206
- puts output # output of g.region (same as last.output)
207
- puts last.status_value # result status of g.region
208
- end
209
- ```
210
-
211
- ### Options
212
-
213
- By default the commands executed in a session are echoed to standard output
214
- (just the command, not its output) and error return status causes
215
- an exception to be raised.
216
-
217
- This behaviour can be changed with some options:
218
-
219
- #### Echo
220
-
221
- Pass `false` as the `:echo` option it you don't want to output
222
- command names and `:output` if you want to output both
223
- the command name and its output.
224
-
225
- ```ruby
226
- GrassGis.session configuration.merge(echo: false) do
227
- # Command names not echoed now ...
228
- end
229
-
230
- GrassGis.session configuration.merge(echo: :output) do
231
- # Command names and its output echoed ...
232
- end
233
- ```
234
-
235
- #### Errors
236
-
237
- To avoid raising exceptions when commands return an error status you can pass
238
- `:quiet` to the `:errors` option. In that case the `error?` method of the
239
- session can be used to check if the previous messatge returned an error status;
240
- `error_info` to get its error message and the status of the command
241
- of the command can be obtained through the `last` command method.
242
-
243
- ```ruby
244
- GrassGis.session configuration.merge(errors: :quiet) do
245
- r.surf.rst 'randpts', elev: 'rstdef', zcol: 'value'
246
- if error?
247
- puts "Last command didn't go well..."
248
- puts "It returned the code: #{last.status_value}"
249
- puts "Here's what it said about the problem:"
250
- puts error_info
251
- end
252
- end
253
- ```
254
-
255
- With the `:quiet` option errors during command execution are not raised,
256
- but if a problem prevents the command from being executed (e.g. the
257
- module does not exist) an exception is still generated. This exception
258
- can be avoided too, with the `:silent` option, intended for tests and
259
- debugging.
260
-
261
- Passing the `:console` value to the `:errors` option is like `:quiet`,
262
- with the additional effect of relaying the command standard error output
263
- to the error output of the script.
264
-
265
- #### Logging
266
-
267
- With the `:log` option you can specify the name of a file
268
- where to record the commands executed and its output.
269
-
270
- ### Technicalities
271
-
272
- #### Session scopes
273
-
274
- In a session block, the Ruby `self` object is altered to
275
- refer to a `GrassGis::Context` object. That means that in addition
276
- to the enclosing `self`, any instance variables of the enclosing
277
- scope are not directly available. This may cause some surprises
278
- but is easy to overcome.
279
-
280
- ```ruby
281
- @value = 10
282
- GrassGis.session configuration do
283
- puts @value # nil!
284
- end
285
- ```
286
-
287
- A possible workaround is to assign instance variables that we need
288
- in the session to local variables:
289
-
290
- ```ruby
291
- @value = 10
292
- value = @value
293
- GrassGis.session configuration do
294
- puts value # 10
295
- end
296
- ```
297
-
298
- To avoid defining these variables you can pass a `:locals` Hash
299
- in the configuration to define values that you need to access
300
- in the session (but you won't be able to assign to them, because
301
- they're not local variables!)
302
-
303
- ```ruby
304
- @value = 10
305
-
306
- GrassGis.session configuration.merge(locals: { value: @value }) do
307
- puts value # 10
308
- value = 11 # don't do this: you're creating a local in the session
309
- end
310
- ```
311
-
312
- A different approach is prevent the session block from using a special
313
- `self` by defining a parameter to the block. This parameter will have
314
- the value of a `GrassGis::Context` which you'll need to explicitly use
315
- to execute any commands:
316
-
317
- ```ruby
318
- @value = 10
319
- GrassGis.session configuration do |grass|
320
- puts @value # 10
321
- grass.g.region res: 10 # now you need to use the object to issue commans
322
- end
323
- ```
324
-
325
- #### Invalid commands
326
-
327
- Currently the generation of GRASS commands inside a session is
328
- implemented in a versy simple way which allows to generate any command
329
- name even if it is invalid or does not exist. This has the advantage
330
- of supporting any version of GRASS, but doesn't allow for early
331
- detection of invalid commands (e.g. due to typos) or invalid command
332
- parameters.
333
-
334
- ```ruby
335
- GrassGis.session configuration do |grass|
336
- g.regoin res: 10 # Oops (runtime error)
337
- g.anything.goes.run # another runtime error
338
- end
339
- ```
340
-
341
- If the command generated does not exist a runtime `ENOENT` exception will
342
- occur.
343
-
344
- If the command exists, then if parameters are not valid, the command
345
- will execute but will return an error status. This will be handled
346
- as explaned above.
347
-
348
- ## Helper methods
349
-
350
- When writing a non-trivial program you'll probably
351
- find you want to define methods to avoid unnecessary repetition.
352
-
353
- Let's see how you can call methdos from your session and be
354
- able to execute GRASS commands from the method in the context of the session.
355
-
356
- Inside a session, `self` refers to an object of class
357
- `GrassGis::Context` which represents the current GRASS session.
358
-
359
- You can invoke grass commands directly on this object, so, if you pass
360
- this object around you can use it to execute GRASS commands:
361
-
362
- ```ruby
363
- def helper_method(grass)
364
- # ...
365
- end
366
-
367
- GrassGis.session configuration do
368
- helper_method self
369
- end
370
- ```
371
-
372
- In the helper method you can use the grass object like this;
373
-
374
- ```ruby
375
- def helper_method(grass)
376
- # change the current region resolution
377
- grass.g.region res: 10
378
- end
379
- ```
380
-
381
- To avoid having to prepend each command with `grapss.` you can
382
- use the `session` method like this:
383
-
384
- ```ruby
385
- def helper_method(grass)
386
- grass.session do
387
- g.region res: 10
388
- g.region '-p'
389
- puts output
390
- end
391
- end
392
- ```
393
-
394
- ### Examples
395
-
396
-
397
- #### 1. Map existence
398
-
399
- Helper methods to check for the existence of maps.
400
-
401
- Often we may want to know if a map exists. Next methods can be used to
402
- check for it.
403
-
404
- ```ruby
405
- def map_exists?(grass, type, map)
406
- grass.g.list type
407
- maps = grass.output.split
408
- maps.include?(map)
409
- end
410
-
411
- def raster_exists?(grass, map)
412
- map_exists? grass, 'rast', map
413
- end
414
-
415
- def vector_exists?(grass, map)
416
- map_exists? grass, 'vect', map
417
- end
418
- ```
419
-
420
- We can use these methods like this:
421
-
422
- ```ruby
423
- GrassGis.session configuration do
424
- unless raster_exists?(self, 'product')
425
- r.mapcalc "product = factor1*factor2"
426
- end
427
- end
428
- ```
429
-
430
- #### 2. Information as Hashes
431
-
432
- Following methods show how to obtain information about a raster map
433
- and the current region as a Hash:
434
-
435
- ```ruby
436
- def raster_info(grass, map)
437
- grass.r.info '-g', map
438
- shell_to_hash grass
439
- end
440
-
441
- def region_info(grass)
442
- grass.g.region '-m'
443
- shell_to_hash grass
444
- end
445
-
446
- def shell_to_hash(grass)
447
- Hash[grass.output.lines.map{|l| l.strip.split('=')}]
448
- end
449
-
450
- # Now, for example, we can easily obtain the resolution of a raster:
451
-
452
- def raster_res(grass, map)
453
- info = raster_info(grass, map)
454
- info.values_at('ewres', 'nsres').map(&:to_i)
455
- end
456
-
457
- def region_res(grass)
458
- info = region_info(grass)
459
- info.values_at('ewres', 'nsres').map(&:to_i)
460
- end
461
- ```
462
-
463
- #### 3. Average angle
464
-
465
- Let's assume we have a raster map `aspect` which is
466
- a direction angle (i.e. a cyclic value from 0 to 360).
467
-
468
- Now imagine that we need to compute a coarser raster grid with
469
- average values per cell. We can't just resample the angle
470
- (we want the average of 359 and 1 be 0, not 180);
471
- we would need an unitary vector or complex number to take averages.
472
-
473
- The next method will perform the average correctly using
474
- auxiliary raster maps for two cartesian components (that represent
475
- the angle as a vector).
476
-
477
- ```ruby
478
- def resample_average_angle(grass, options = {})
479
- input_raster = options[:input]
480
- raise "Raster #{input_raster} not found" unless raster_exists?(grass, input_raster)
481
- input_res = raster_res(grass, input_raster)
482
-
483
- if options[:output_res]
484
- output_res = options[:output_res]
485
- unless output_res.is_a?(Array)
486
- output_res = [output_res, output_res]
487
- end
488
- else
489
- output_res = region_res(grass)
490
- end
491
-
492
- output_raster = options[:output]
493
-
494
- grass.session do
495
- unless raster_exists?(self, "#{input_raster}_sin")
496
- g.region ewres: input_res[0], nsres: input_res[1]
497
- r.mapcalc "#{input_raster}_sin = sin(#{input_raster})"
498
- end
499
- unless raster_exists?(self, "#{input_raster}_cos")
500
- g.region ewres: input_res[0], nsres: input_res[1]
501
- r.mapcalc "#{input_raster}_cos = cos(#{input_raster})"
502
- end
503
- g.region ewres: output_res[0], nsres: output_res[1]
504
- r.resamp.stats input: "#{input_raster}_cos", output: "#{output_raster}_cos"
505
- r.resamp.stats input: "#{input_raster}_sin", output: "#{output_raster}_sin"
506
- r.mapcalc "#{output_raster} = atan(#{output_raster}_cos,#{output_raster}_sin)"
507
- r.colors map: ouput_raster, raster: input_raster
508
- g.remove '-f', type: 'raster', name: ["#{output_raster}_cos", "#{output_raster}_sin"]
509
- g.remove -f type=raster name=aspect_sin@landscape,aspect_cos@landscape
510
- end
511
- end
512
- ```
513
-
514
- Now, to resample a (cyclic angular) map `aspect_hires` to a lower resolution 10:
515
-
516
- ```ruby
517
- GrassGis.session configuration do
518
- resamp_average self,
519
- input: 'aspect_hires',
520
- output: 'aspect_lowres', output_res: 10
521
- end
522
- end
523
- ```
524
-
525
- ## Roadmap
526
-
527
- * Change Module to define explicitly available GRASS commands instead of
528
- accepting anything with `method_missing`. Declare commands with permitted
529
- arguments and options, etc.
530
- * Add some session helpers:
531
- - Method to clean GRASS temporaries ($GISBASE/etc/clean_temp), or do
532
- it automatically when disposing the session.
533
- - Methods to check if maps exist
534
- - Methods that return information as objects (arrays, hashes), e.g.
535
- values returned by r.what, the current region, etc.
536
- - Methods that execute operations in a GRASS-version independent
537
- manner (higher level, version independent interface to modules).
538
-
539
- ### GRASS cooking DSL
540
-
541
- Create a DSL to define GRASS processing recipes.
542
-
543
- It should handle errors gracefully during recipe execution,
544
- take care of logging, parsing command output, temporary files, etc.
545
-
546
- A recipe could have requirements such as which other recipes it depends
547
- upon (so must be executed first), which map layers must exist, etc.
548
-
549
- It could also define what map layers or other files are generated by the
550
- recipe.
551
-
552
- Example of intended recipe syntax:
553
-
554
- ```ruby
555
- recipe :resamp_average do |options = {}|
556
- # ...
557
- end
558
-
559
- recipe :generate_working_dem do |...|
560
- define :working_dem, 'dem'
561
- define :working_slope, 'slope'
562
- define :working_aspect, 'aspect'
563
- g.region res: working_res
564
- apply :resamp_average, input: base_dem, output: working_dem, input_res: base_dem_res, output_res: working_res
565
- apply :resamp_average, input: base_slope, output: working_slope, input_res: base_dem_res, output_res: working_res
566
- apply :resamp_average, input: base_aspect, output: working_aspect, input_res: base_dem_res, output_res: working_res, direction: true
567
- describe working_slope, "Pendiente en grados a #{working_res} #{region_units}"
568
- # ...
569
- end
570
- ```
571
-
572
- ## Contributing
573
-
574
- 1. Fork it ( https://github.com/[my-github-username]/grassgis/fork )
575
- 2. Create your feature branch (`git checkout -b my-new-feature`)
576
- 3. Commit your changes (`git commit -am 'Add some feature'`)
577
- 4. Push to the branch (`git push origin my-new-feature`)
578
- 5. Create a new Pull Request
1
+ # GrassGis
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/grassgis.svg)](http://badge.fury.io/rb/grassgis)
4
+ [![Build Status](https://travis-ci.org/jgoizueta/grassgis.svg)](https://travis-ci.org/jgoizueta/grassgis)
5
+
6
+ Support for scripting GRASS with Ruby.
7
+
8
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
9
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
10
+ **Table of Contents**
11
+
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [Configuration](#configuration)
15
+ - [Running a GRASS Session](#running-a-grass-session)
16
+ - [Creating new locations and mapsets](#creating-new-locations-and-mapsets)
17
+ - [History](#history)
18
+ - [Options](#options)
19
+ - [Echo](#echo)
20
+ - [Errors](#errors)
21
+ - [Logging](#logging)
22
+ - [Recipes](#recipes)
23
+ - [Technicalities](#technicalities)
24
+ - [Session scopes](#session-scopes)
25
+ - [Invalid commands](#invalid-commands)
26
+ - [Helper methods](#helper-methods)
27
+ - [Examples](#examples)
28
+ - [1. Map existence](#1-map-existence)
29
+ - [2. Information as Hashes](#2-information-as-hashes)
30
+ - [3. Average angle](#3-average-angle)
31
+ - [Roadmap](#roadmap)
32
+ - [Contributing](#contributing)
33
+
34
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ gem 'grassgis'
41
+
42
+ And then execute:
43
+
44
+ $ bundle
45
+
46
+ Or install it yourself as:
47
+
48
+ $ gem install grassgis
49
+
50
+ ## Usage
51
+
52
+ This library can prepare environments to execute GRASS commands
53
+ from Ruby scripts.
54
+
55
+ First we require the library:
56
+
57
+ ```ruby
58
+ require 'grassgis'
59
+ ```
60
+
61
+ ### Configuration
62
+
63
+ A GRASS session operates on a given location and mapset.
64
+
65
+ Before starting a GRASS session we need some configuration
66
+ parameters to specify the location of the GRASS Installation
67
+ to be used and the location/mapset. We do this by using
68
+ a Ruby Hash containing configuration parameters:
69
+
70
+ ```ruby
71
+ configuration = {
72
+ gisbase: '/usr/local/grass-7.0.0',
73
+ gisdbase: File.join(ENV['HOME'], 'grassdata'),
74
+ location: 'nc_spm',
75
+ mapset: 'user1'
76
+ }
77
+ ```
78
+
79
+ So, you first need to know where is GRASS installed on your system
80
+ to define the `:gisbase` option to point to the base directory of the GRASS
81
+ installation.
82
+
83
+ For *Ubuntu* it will typically be `/usr/lib/grass70` for version 7 of GRASS.
84
+
85
+ In *Windows*, if installed with
86
+ [OSGeo4W](http://trac.osgeo.org/osgeo4w/) it is typically of
87
+ the form `C:\OGeo4W\app\grass\grass-7.0.0` (the last directory will vary
88
+ depending on the GRASS version).
89
+
90
+ Under *Mac OS X*, if using [Homebrew](http://brew.sh/)
91
+ (with the [osgeo/osgeo4mac](https://github.com/OSGeo/homebrew-osgeo4mac) tap)
92
+ it should bd something like `/usr/local/Cellar/grass-70/7.0.0/grass-7.0.0`.
93
+
94
+ You can find the `:gisbase` directory by executing the
95
+ `grass` command of your system (which may be `grass70`, `grass64`, etc.)
96
+ with the `--config path` option:
97
+
98
+ grass --config path
99
+
100
+ You must also specify the `GISDBASE`, `LOCATION` and `MAPSET`, to work with,
101
+ just like when starting GRASS, through the `:gisdbase`, `:location` and
102
+ `:mapset` configuration options.
103
+ You can omit `:gisdbase` which will default to a directory named `grassdata` in the
104
+ user's home directory and `:mapset` which defaults to `PERMANENT`.
105
+
106
+
107
+ ### Running a GRASS Session
108
+
109
+ With the proper configuration in place, we can use it to
110
+ create a GRASS Session and execute GRASS command from it:
111
+
112
+ ```ruby
113
+ GrassGis.session configuration do
114
+ g.list 'vect'
115
+ puts output # will print list of vector maps
116
+ end
117
+ ```
118
+
119
+ Inside a `GrassGis` session we can execute GRASS commands
120
+ just by using the command name as a Ruby method.
121
+
122
+ Command flags and options must be passed first as regular method arguments,
123
+ then named parameters must be passed as a Ruby Hash:
124
+
125
+ ```ruby
126
+ g.region '-p', rast: 'elevation'
127
+ d.rast 'elevation'
128
+ d.vect 'streams', col: 'blue'
129
+ ```
130
+
131
+ If you try to execute an invalid module name an `ENOENT`
132
+ error will be raised:
133
+
134
+ ```ruby
135
+ g.this.module.does.not.exist '???'
136
+ ```
137
+
138
+ If the command to be executed has no arguments you need
139
+ to invoke `.run` on it to execute it:
140
+
141
+ ```ruby
142
+ d.erase.run
143
+ g.list.run
144
+ ```
145
+
146
+ ### Creating new locations and mapsets
147
+
148
+ To create a new location and/or mapset, open a session to it
149
+ and use a `:create` parameter like this:
150
+
151
+ ```ruby
152
+ options = configuration.merge(
153
+ location: 'new_location',
154
+ mapset: 'new_mapset',
155
+ create: {
156
+ epsg: 4326 # coordinate system for the new location
157
+ limits: [-5, 30, 5, 50], # optional E, S, W, N limits
158
+ res: 2 # optional resolution
159
+ }
160
+ )
161
+ GrassGis.session options do
162
+ g.region '-p'
163
+ puts output
164
+ end
165
+ ```
166
+
167
+ Use `nil` or `PERMANENT` for the mapset to avoid creating a new mapset.
168
+
169
+ Existing locations or mapsets are not changed.
170
+
171
+ ### History
172
+
173
+ The return value of a GRASS command invocation inside a session is
174
+ a `SysCmd::Command`
175
+ (see the [sys_cmd gem](https://github.com/jgoizueta/sys_cmd)).
176
+
177
+ ```ruby
178
+ GrassGis.session configuration do+
179
+ cmd = g.region '-p'
180
+ puts cmd.output # command output is kept in the Command object
181
+ puts cmd.status_value # 0 for success
182
+ end
183
+ ```
184
+
185
+ You don't need to assign commands to variables as in the example
186
+ to access them, because they're all kept in an array accesible
187
+ through the `history` method of the session:
188
+
189
+ ```ruby
190
+ GrassGis.session configuration do+
191
+ r.info 'slope'
192
+ g.region '-p'
193
+ puts history.size # 2
194
+ puts history[-1].output # output of g.region
195
+ puts history[-2].output # output of r.info
196
+ end
197
+ ```
198
+
199
+ The last executed command (`history[-1]`) is also accessible through
200
+ the `last` method and its output through `output`:
201
+
202
+ ```ruby
203
+ GrassGis.session configuration do+
204
+ r.info 'slope'
205
+ g.region '-p'
206
+ puts output # output of g.region (same as last.output)
207
+ puts last.status_value # result status of g.region
208
+ end
209
+ ```
210
+
211
+ ### Options
212
+
213
+ By default the commands executed in a session are echoed to standard output
214
+ (just the command, not its output) and error return status causes
215
+ an exception to be raised.
216
+
217
+ This behaviour can be changed with some options:
218
+
219
+ #### Echo
220
+
221
+ Pass `false` as the `:echo` option it you don't want to output
222
+ command names and `:output` if you want to output both
223
+ the command name and its output.
224
+
225
+ ```ruby
226
+ GrassGis.session configuration.merge(echo: false) do
227
+ # Command names not echoed now ...
228
+ end
229
+
230
+ GrassGis.session configuration.merge(echo: :output) do
231
+ # Command names and its output echoed ...
232
+ end
233
+ ```
234
+
235
+ #### Errors
236
+
237
+ To avoid raising exceptions when commands return an error status you can pass
238
+ `:quiet` to the `:errors` option. In that case the `error?` method of the
239
+ session can be used to check if the previous messatge returned an error status;
240
+ `error_info` to get its error message and the status of the command
241
+ of the command can be obtained through the `last` command method.
242
+
243
+ ```ruby
244
+ GrassGis.session configuration.merge(errors: :quiet) do
245
+ r.surf.rst 'randpts', elev: 'rstdef', zcol: 'value'
246
+ if error?
247
+ puts "Last command didn't go well..."
248
+ puts "It returned the code: #{last.status_value}"
249
+ puts "Here's what it said about the problem:"
250
+ puts error_info
251
+ end
252
+ end
253
+ ```
254
+
255
+ With the `:quiet` option errors during command execution are not raised,
256
+ but if a problem prevents the command from being executed (e.g. the
257
+ module does not exist) an exception is still generated. This exception
258
+ can be avoided too, with the `:silent` option, intended for tests and
259
+ debugging.
260
+
261
+ Passing the `:console` value to the `:errors` option is like `:quiet`,
262
+ with the additional effect of relaying the command standard error output
263
+ to the error output of the script.
264
+
265
+ #### Logging
266
+
267
+ With the `:log` option you can specify the name of a file
268
+ where to record the commands executed and its output.
269
+
270
+ ### Recipes
271
+
272
+ The `GrassCookbook` interface can be used to define geoprocessing
273
+ "recipes", each one specifying which data is required and
274
+ produced by the process.
275
+
276
+ A recipe is defined by calling `GrassCookbook.recipe` with a block
277
+ that provides the recipe definition in a declarative way, using
278
+ methods such as `description`, `required_parameters`, `required_files`,
279
+ `required_raster_maps`, `generated_rater_maps`, etc.
280
+
281
+ The `process` method defines, by using a block, the recipes's procedure.
282
+ The arguments to this block will be taken auto-magically from parameters
283
+ of the same name (parameters are provided to a recipe-executing environment
284
+ through a Hash).
285
+
286
+ The available `GrassCookbook` methods can be used to determine which recipes need
287
+ to be executed, in which order, and which products will be generated
288
+ based on available data.
289
+
290
+ For example, given this three recipes:
291
+
292
+ ```ruby
293
+ GrassCookbook.recipe :dem_base_from_mdt05 do
294
+ description %{
295
+ Generate a DEM for the location region at fixed 5m resolution
296
+ from CNIG's MDT05 data.
297
+ }
298
+
299
+ required_files 'data/MDT05'
300
+ generated_raster_maps 'dem_base'
301
+
302
+ process do |mdt05_sheets|
303
+ # Import MDT05 sheets and generate dem_base
304
+ mdt05_sheets = mdt05_sheets.map { |n| "MDT05-#{n}-H30-LIDAR.asc"}
305
+ sheet_maps = Hash[mdt05_sheets.map { |s| [s, File.basename(s, '.asc')]}]
306
+ mdt05_sheets.each do |sheet|
307
+ map = sheet_maps[sheet]
308
+ r.in.gdal '-o', '--overwrite',
309
+ input: File.join('data', 'MDT05', sheet),
310
+ output: map
311
+ r.colors map: map, color: 'elevation'
312
+ end
313
+ # Keep previous resolution
314
+ ewres, nsres = region_res
315
+ # Patch sheets and crop
316
+ g.region res: 5
317
+ r.patch input: sheet_maps.values, output: 'dem_base'
318
+ r.colors map: 'dem_base', color: 'elevation'
319
+ # Restore previous resolution
320
+ g.region nsres: nsres, ewres: ewres
321
+ g.remove '-f', type: 'raster', name: sheet_maps.values
322
+ end
323
+ end
324
+
325
+ GrassCookbook.recipe :dem_base_derived do
326
+ description %{
327
+ Generate DEM-derived maps from the 5m dem base:
328
+ slope, aspect and relief shading
329
+ }
330
+
331
+ required_raster_maps 'dem_base'
332
+ generated_raster_maps 'shade_base', 'slope_base', 'aspect_base'
333
+
334
+ process do
335
+ r.relief input: 'dem_base', output: 'shade_base'
336
+ r.slope.aspect elevation: 'dem_base',
337
+ slope: 'slope_base',
338
+ aspect: 'aspect_base'
339
+ end
340
+ end
341
+
342
+ GrassCookbook.recipe :working_dem do
343
+ description %{
344
+ Generate DEM data at working resolution
345
+ }
346
+
347
+ required_raster_maps 'dem_base', 'slope_base', 'aspect_base'
348
+ generated_raster_maps 'dem', 'slope', 'aspect'
349
+
350
+ process do |resolution|
351
+ # Keep previous resolution
352
+ ewres, nsres = region_res
353
+
354
+ resamp_average input: 'dem_base', output: 'dem', output_res: resolution
355
+ resamp_average input: 'slope_base', output: 'slope', output_res: resolution
356
+ resamp_average input: 'aspect_base', output: 'aspect', output_res: resolution, direction: true
357
+
358
+ # Restore previous resolution
359
+ g.region nsres: nsres, ewres: ewres
360
+ end
361
+ end
362
+ ```
363
+
364
+ We could now use those recipes to compute some permanent base maps and
365
+ then maps for alternative scenarios by varying some parameter.
366
+
367
+ In this example the fixed parameter defines the available data
368
+ we have to create a base DEM at a resolution of 5 meters,
369
+ which will be kept in the PERMANENT mapset.
370
+
371
+ Then we vary the `resolution` parameter to compute derived information
372
+ (topography information at the given resolution) for two values
373
+ of the parameter (10m and 25m) which will produce two mapsets with the
374
+ name assigned to the variant scenario ('10m' and '25m')
375
+ and all the maps that depend on the varying parameter in each of them.
376
+
377
+ ```ruby
378
+ GrassGis.session grass_config do
379
+ # First generate maps using fixed parameters and move them to PERMANENT
380
+ fixed_parameters = { mdt05_sheets: %w(0244 0282) }
381
+ fixed_data = primary = GrassCookbook::Data[
382
+ parameters: fixed_parameters.keys,
383
+ files: GrassCookbook.existing_external_input_files
384
+ ]
385
+ plan = GrassCookbook.plan(fixed_data)
386
+ permanent = plan.last
387
+ GrassCookbook.replace_existing_products self, plan
388
+ GrassCookbook.execute self, fixed_parameters, plan
389
+ permanent.maps.each do |map, type|
390
+ move_map(map, type: type, to: 'PERMANENT')
391
+ end
392
+
393
+ # Then define some variations of other parameters and create a mapset
394
+ # for each variation, where maps dependent on the varying parameters
395
+ # will be put
396
+ variants = {
397
+ '10m' => { resolution: 10 },
398
+ '25m' => { resolution: 25 }
399
+ }
400
+ for variant_name, variant_parameters in variants
401
+ data = GrassCookbook::Data[parameters: variant_parameters.keys] + permanent
402
+ plan = GrassCookbook.plan(data)
403
+ GrassCookbook.replace_existing_products self, plan
404
+ GrassCookbook.execute self, fixed_parameters.merge(variant_parameters), plan
405
+ variant_maps = (plan.last - data).maps
406
+ create_mapset variant_name
407
+ variant_maps.each do |map, type|
408
+ move_map(map, type: type, to: variant_name)
409
+ end
410
+ end
411
+ end
412
+ ```
413
+
414
+ ### Technicalities
415
+
416
+ #### Session scopes
417
+
418
+ In a session block, the Ruby `self` object is altered to
419
+ refer to a `GrassGis::Context` object. That means that in addition
420
+ to the enclosing `self`, any instance variables of the enclosing
421
+ scope are not directly available. This may cause some surprises
422
+ but is easy to overcome.
423
+
424
+ ```ruby
425
+ @value = 10
426
+ GrassGis.session configuration do
427
+ puts @value # nil!
428
+ end
429
+ ```
430
+
431
+ A possible workaround is to assign instance variables that we need
432
+ in the session to local variables:
433
+
434
+ ```ruby
435
+ @value = 10
436
+ value = @value
437
+ GrassGis.session configuration do
438
+ puts value # 10
439
+ end
440
+ ```
441
+
442
+ To avoid defining these variables you can pass a `:locals` Hash
443
+ in the configuration to define values that you need to access
444
+ in the session (but you won't be able to assign to them, because
445
+ they're not local variables!)
446
+
447
+ ```ruby
448
+ @value = 10
449
+
450
+ GrassGis.session configuration.merge(locals: { value: @value }) do
451
+ puts value # 10
452
+ value = 11 # don't do this: you're creating a local in the session
453
+ end
454
+ ```
455
+
456
+ A different approach is prevent the session block from using a special
457
+ `self` by defining a parameter to the block. This parameter will have
458
+ the value of a `GrassGis::Context` which you'll need to explicitly use
459
+ to execute any commands:
460
+
461
+ ```ruby
462
+ @value = 10
463
+ GrassGis.session configuration do |grass|
464
+ puts @value # 10
465
+ grass.g.region res: 10 # now you need to use the object to issue commands
466
+ end
467
+ ```
468
+
469
+ The GRASS command `g.mapset` should not be used to change
470
+ the current mapset, use the `change_mapset` method in a GrassGis
471
+ session instead:
472
+
473
+ ```ruby
474
+ GrassGis.session configuration do
475
+ # Get the name of the current mapset
476
+ g.mapsets '-p'
477
+ mapset = output.lines.last.split.first.inspect
478
+ # Copy 'some_map' raster map to PERMANENT
479
+ change_mapset 'PERMANENT'
480
+ g.copy rast: "some_map@#{original_mapset},some_map"
481
+ # Get back to our mapset
482
+ change_mapset mapset
483
+ end
484
+ ```
485
+
486
+ #### Invalid commands
487
+
488
+ Currently the generation of GRASS commands inside a session is
489
+ implemented in a very simple way which allows to generate any command
490
+ name even if it is invalid or does not exist. This has the advantage
491
+ of supporting any version of GRASS, but doesn't allow for early
492
+ detection of invalid commands (e.g. due to typos) or invalid command
493
+ parameters.
494
+
495
+ ```ruby
496
+ GrassGis.session configuration do |grass|
497
+ g.region res: 10 # Oops (runtime error)
498
+ g.anything.goes.run # another runtime error
499
+ end
500
+ ```
501
+
502
+ If the command generated does not exist a runtime `ENOENT` exception will
503
+ occur.
504
+
505
+ If the command exists, then if parameters are not valid, the command
506
+ will execute but will return an error status. This will be handled
507
+ as explained above.
508
+
509
+ ## Helper methods
510
+
511
+ When writing a non-trivial program you'll probably
512
+ find you want to define methods to avoid unnecessary repetition.
513
+
514
+ Let's see how you can call methods from your session and be
515
+ able to execute GRASS commands from the method in the context of the session.
516
+
517
+ Inside a session, `self` refers to an object of class
518
+ `GrassGis::Context` which represents the current GRASS session.
519
+
520
+ You can invoke grass commands directly on this object, so, if you pass
521
+ this object around you can use it to execute GRASS commands:
522
+
523
+ ```ruby
524
+ def helper_method(grass)
525
+ # ...
526
+ end
527
+
528
+ GrassGis.session configuration do
529
+ helper_method self
530
+ end
531
+ ```
532
+
533
+ In the helper method you can use the grass object like this;
534
+
535
+ ```ruby
536
+ def helper_method(grass)
537
+ # change the current region resolution
538
+ grass.g.region res: 10
539
+ end
540
+ ```
541
+
542
+ To avoid having to prepend each command with `grass.` you can
543
+ use the `session` method like this:
544
+
545
+ ```ruby
546
+ def helper_method(grass)
547
+ grass.session do
548
+ g.region res: 10
549
+ g.region '-p'
550
+ puts output
551
+ end
552
+ end
553
+ ```
554
+
555
+ An alternative is to use a Ruby module and extend the session with it:
556
+
557
+ ```ruby
558
+ module Helpers
559
+ def helper_method
560
+ g.region res: 10
561
+ g.region '-p'
562
+ puts output
563
+ end
564
+ end
565
+
566
+ GrassGis.session configuration do
567
+ extend Helpers
568
+ helper_method
569
+ end
570
+ ```
571
+
572
+ ### Examples
573
+
574
+ Note: the functionality of these examples is now provided by
575
+ a Tools module which is included by default in GrassGis sessions.
576
+
577
+ #### 1. Map existence
578
+
579
+ Helper methods to check for the existence of maps.
580
+
581
+ Often we may want to know if a map exists. Next methods can be used to
582
+ check for it.
583
+
584
+ ```ruby
585
+ def map_exists?(grass, type, map)
586
+ grass.g.list type
587
+ maps = grass.output.split
588
+ maps.include?(map)
589
+ end
590
+
591
+ def raster_exists?(grass, map)
592
+ map_exists? grass, 'rast', map
593
+ end
594
+
595
+ def vector_exists?(grass, map)
596
+ map_exists? grass, 'vect', map
597
+ end
598
+ ```
599
+
600
+ We can use these methods like this:
601
+
602
+ ```ruby
603
+ GrassGis.session configuration do
604
+ unless raster_exists?(self, 'product')
605
+ r.mapcalc "product = factor1*factor2"
606
+ end
607
+ end
608
+ ```
609
+
610
+ #### 2. Information as Hashes
611
+
612
+ Following methods show how to obtain information about a raster map
613
+ and the current region as a Hash:
614
+
615
+ ```ruby
616
+ def raster_info(grass, map)
617
+ grass.r.info '-g', map
618
+ shell_to_hash grass
619
+ end
620
+
621
+ def region_info(grass)
622
+ grass.g.region '-m'
623
+ shell_to_hash grass
624
+ end
625
+
626
+ def shell_to_hash(grass)
627
+ Hash[grass.output.lines.map{|l| l.strip.split('=')}]
628
+ end
629
+
630
+ # Now, for example, we can easily obtain the resolution of a raster:
631
+
632
+ def raster_res(grass, map)
633
+ info = raster_info(grass, map)
634
+ info.values_at('ewres', 'nsres').map(&:to_i)
635
+ end
636
+
637
+ def region_res(grass)
638
+ info = region_info(grass)
639
+ info.values_at('ewres', 'nsres').map(&:to_i)
640
+ end
641
+ ```
642
+
643
+ #### 3. Average angle
644
+
645
+ Let's assume we have a raster map `aspect` which is
646
+ a direction angle (i.e. a cyclic value from 0 to 360).
647
+
648
+ Now imagine that we need to compute a coarser raster grid with
649
+ average values per cell. We can't just resample the angle
650
+ (we want the average of 359 and 1 be 0, not 180);
651
+ we would need an unitary vector or complex number to take averages.
652
+
653
+ The next method will perform the average correctly using
654
+ auxiliary raster maps for two cartesian components (that represent
655
+ the angle as a vector).
656
+
657
+ ```ruby
658
+ def resample_average_angle(grass, options = {})
659
+ input_raster = options[:input]
660
+ raise "Raster #{input_raster} not found" unless raster_exists?(grass, input_raster)
661
+ input_res = raster_res(grass, input_raster)
662
+
663
+ if options[:output_res]
664
+ output_res = options[:output_res]
665
+ unless output_res.is_a?(Array)
666
+ output_res = [output_res, output_res]
667
+ end
668
+ else
669
+ output_res = region_res(grass)
670
+ end
671
+
672
+ output_raster = options[:output]
673
+
674
+ grass.session do
675
+ unless raster_exists?(self, "#{input_raster}_sin")
676
+ g.region ewres: input_res[0], nsres: input_res[1]
677
+ r.mapcalc "#{input_raster}_sin = sin(#{input_raster})"
678
+ end
679
+ unless raster_exists?(self, "#{input_raster}_cos")
680
+ g.region ewres: input_res[0], nsres: input_res[1]
681
+ r.mapcalc "#{input_raster}_cos = cos(#{input_raster})"
682
+ end
683
+ g.region ewres: output_res[0], nsres: output_res[1]
684
+ r.resamp.stats input: "#{input_raster}_cos", output: "#{output_raster}_cos"
685
+ r.resamp.stats input: "#{input_raster}_sin", output: "#{output_raster}_sin"
686
+ r.mapcalc "#{output_raster} = atan(#{output_raster}_cos,#{output_raster}_sin)"
687
+ r.colors map: ouput_raster, raster: input_raster
688
+ g.remove '-f', type: 'raster', name: ["#{output_raster}_cos", "#{output_raster}_sin"]
689
+ g.remove -f type=raster name=aspect_sin@landscape,aspect_cos@landscape
690
+ end
691
+ end
692
+ ```
693
+
694
+ Now, to resample a (cyclic angular) map `aspect_hires` to a lower resolution 10:
695
+
696
+ ```ruby
697
+ GrassGis.session configuration do
698
+ resamp_average self,
699
+ input: 'aspect_hires',
700
+ output: 'aspect_lowres', output_res: 10
701
+ end
702
+ end
703
+ ```
704
+
705
+ ## Roadmap
706
+
707
+ * Change Module to define explicitly available GRASS commands instead of
708
+ accepting anything with `method_missing`. Declare commands with permitted
709
+ arguments and options, etc.
710
+ * Add some session helpers:
711
+ - Method to clean GRASS temporaries ($GISBASE/etc/clean_temp), or do
712
+ it automatically when disposing the session.
713
+ - Methods that execute operations in a GRASS-version independent
714
+ manner (higher level, version independent interface to modules).
715
+
716
+ ## Contributing
717
+
718
+ 1. Fork it ( https://github.com/[my-github-username]/grassgis/fork )
719
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
720
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
721
+ 4. Push to the branch (`git push origin my-new-feature`)
722
+ 5. Create a new Pull Request