grassgis 0.4.1 → 0.4.2

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.
data/README.md CHANGED
@@ -1,577 +1,578 @@
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
- In *Windows*, if installed with
85
- [OSGeo4W](http://trac.osgeo.org/osgeo4w/) it is typically of
86
- the form `C:\OGeo4W\app\grass\grass-7.0.0` (the last directory will vary
87
- depending on the GRASS version).
88
-
89
- Under *Mac OS X*, if using [Homebrew](http://brew.sh/)
90
- (with the [osgeo/osgeo4mac](https://github.com/OSGeo/homebrew-osgeo4mac) tap)
91
- it should bd something like `/usr/local/Cellar/grass-70/7.0.0/grass-7.0.0`.
92
-
93
- You can find the `:gisbase` directory by executing the
94
- `grass` command of your system (which may be `grass70`, `grass64`, etc.)
95
- with the `--config path` option:
96
-
97
- grass --config path
98
-
99
- You must also specify the `GISDBASE`, `LOCATION` and `MAPSET`, to work with,
100
- just like when starting GRASS, through the `:gisdbase`, `:location` and
101
- `:mapset` configuration options.
102
- You can omit `:gisdbase` which will default to a directory named `grassdata` in the
103
- user's home directory and `:mapset` which defaults to `PERMANENT`.
104
-
105
-
106
- ### Running a GRASS Session
107
-
108
- With the proper configuration in place, we can use it to
109
- create a GRASS Session and execute GRASS command from it:
110
-
111
- ```ruby
112
- GrassGis.session configuration do
113
- g.list 'vect'
114
- puts output # will print list of vector maps
115
- end
116
- ```
117
-
118
- Inside a `GrassGis` session we can execute GRASS commands
119
- just by using the command name as a Ruby method.
120
-
121
- Command flags and options must be passed first as regular method arguments,
122
- then named parameters must be passed as a Ruby Hash:
123
-
124
- ```ruby
125
- g.region '-p', rast: 'elevation'
126
- d.rast 'elevation'
127
- d.vect 'streams', col: 'blue'
128
- ```
129
-
130
- If you try to execute an invalid module name an `ENOENT`
131
- error will be raised:
132
-
133
- ```ruby
134
- g.this.module.does.not.exist '???'
135
- ```
136
-
137
- If the command to be executed has no arguments you need
138
- to invoke `.run` on it to execute it:
139
-
140
- ```ruby
141
- d.erase.run
142
- g.list.run
143
- ```
144
-
145
- ### Creating new locations and mapsets
146
-
147
- To create a new location and/or mapset, open a session to it
148
- and use a `:create` parameter like this:
149
-
150
- ```ruby
151
- options = configuration.merge(
152
- location: 'new_location',
153
- mapset: 'new_mapset',
154
- create: {
155
- epsg: 4326 # coordinate system for the new location
156
- limits: [-5, 30, 5, 50], # optional E, S, W, N limits
157
- res: 2 # optional resolution
158
- }
159
- )
160
- GrassGis.session options do
161
- g.region '-p'
162
- puts output
163
- end
164
- ```
165
-
166
- Use `nil` or `PERMANENT` for the mapset to avoid creating a new mapset.
167
-
168
- Existing locations or mapsets are not changed.
169
-
170
- ### History
171
-
172
- The return value of a GRASS command invocation inside a session is
173
- a `SysCmd::Command`
174
- (see the [sys_cmd gem](https://github.com/jgoizueta/sys_cmd)).
175
-
176
- ```ruby
177
- GrassGis.session configuration do+
178
- cmd = g.region '-p'
179
- puts cmd.output # command output is kept in the Command object
180
- puts cmd.status_value # 0 for success
181
- end
182
- ```
183
-
184
- You don't need to assign commands to variables as in the example
185
- to access them, because they're all kept in an array accesible
186
- through the `history` method of the session:
187
-
188
- ```ruby
189
- GrassGis.session configuration do+
190
- r.info 'slope'
191
- g.region '-p'
192
- puts history.size # 2
193
- puts history[-1].output # output of g.region
194
- puts history[-2].output # output of r.info
195
- end
196
- ```
197
-
198
- The last executed command (`history[-1]`) is also accessible through
199
- the `last` method and its output through `output`:
200
-
201
- ```ruby
202
- GrassGis.session configuration do+
203
- r.info 'slope'
204
- g.region '-p'
205
- puts output # output of g.region (same as last.output)
206
- puts last.status_value # result status of g.region
207
- end
208
- ```
209
-
210
- ### Options
211
-
212
- By default the commands executed in a session are echoed to standard output
213
- (just the command, not its output) and error return status causes
214
- an exception to be raised.
215
-
216
- This behaviour can be changed with some options:
217
-
218
- #### Echo
219
-
220
- Pass `false` as the `:echo` option it you don't want to output
221
- command names and `:output` if you want to output both
222
- the command name and its output.
223
-
224
- ```ruby
225
- GrassGis.session configuration.merge(echo: false) do
226
- # Command names not echoed now ...
227
- end
228
-
229
- GrassGis.session configuration.merge(echo: :output) do
230
- # Command names and its output echoed ...
231
- end
232
- ```
233
-
234
- #### Errors
235
-
236
- To avoid raising exceptions when commands return an error status you can pass
237
- `:quiet` to the `:errors` option. In that case the `error?` method of the
238
- session can be used to check if the previous messatge returned an error status;
239
- `error_info` to get its error message and the status of the command
240
- of the command can be obtained through the `last` command method.
241
-
242
- ```ruby
243
- GrassGis.session configuration.merge(errors: :quiet) do
244
- r.surf.rst 'randpts', elev: 'rstdef', zcol: 'value'
245
- if error?
246
- puts "Last command didn't go well..."
247
- puts "It returned the code: #{last.status_value}"
248
- puts "Here's what it said about the problem:"
249
- puts error_info
250
- end
251
- end
252
- ```
253
-
254
- With the `:quiet` option errors during command execution are not raised,
255
- but if a problem prevents the command from being executed (e.g. the
256
- module does not exist) an exception is still generated. This exception
257
- can be avoided too, with the `:silent` option, intended for tests and
258
- debugging.
259
-
260
- Passing the `:console` value to the `:errors` option is like `:quiet`,
261
- with the additional effect of relaying the command standard error output
262
- to the error output of the script.
263
-
264
- #### Logging
265
-
266
- With the `:log` option you can specify the name of a file
267
- where to record the commands executed and its output.
268
-
269
- ### Technicalities
270
-
271
- #### Session scopes
272
-
273
- In a session block, the Ruby `self` object is altered to
274
- refer to a `GrassGis::Context` object. That means that in addition
275
- to the enclosing `self`, any instance variables of the enclosing
276
- scope are not directly available. This may cause some surprises
277
- but is easy to overcome.
278
-
279
- ```ruby
280
- @value = 10
281
- GrassGis.session configuration do
282
- puts @value # nil!
283
- end
284
- ```
285
-
286
- A possible workaround is to assign instance variables that we need
287
- in the session to local variables:
288
-
289
- ```ruby
290
- @value = 10
291
- value = @value
292
- GrassGis.session configuration do
293
- puts value # 10
294
- end
295
- ```
296
-
297
- To avoid defining these variables you can pass a `:locals` Hash
298
- in the configuration to define values that you need to access
299
- in the session (but you won't be able to assign to them, because
300
- they're not local variables!)
301
-
302
- ```ruby
303
- @value = 10
304
-
305
- GrassGis.session configuration.merge(locals: { value: @value }) do
306
- puts value # 10
307
- value = 11 # don't do this: you're creating a local in the session
308
- end
309
- ```
310
-
311
- A different approach is prevent the session block from using a special
312
- `self` by defining a parameter to the block. This parameter will have
313
- the value of a `GrassGis::Context` which you'll need to explicitly use
314
- to execute any commands:
315
-
316
- ```ruby
317
- @value = 10
318
- GrassGis.session configuration do |grass|
319
- puts @value # 10
320
- grass.g.region res: 10 # now you need to use the object to issue commans
321
- end
322
- ```
323
-
324
- #### Invalid commands
325
-
326
- Currently the generation of GRASS commands inside a session is
327
- implemented in a versy simple way which allows to generate any command
328
- name even if it is invalid or does not exist. This has the advantage
329
- of supporting any version of GRASS, but doesn't allow for early
330
- detection of invalid commands (e.g. due to typos) or invalid command
331
- parameters.
332
-
333
- ```ruby
334
- GrassGis.session configuration do |grass|
335
- g.regoin res: 10 # Oops (runtime error)
336
- g.anything.goes.run # another runtime error
337
- end
338
- ```
339
-
340
- If the command generated does not exist a runtime `ENOENT` exception will
341
- occur.
342
-
343
- If the command exists, then if parameters are not valid, the command
344
- will execute but will return an error status. This will be handled
345
- as explaned above.
346
-
347
- ## Helper methods
348
-
349
- When writing a non-trivial program you'll probably
350
- find you want to define methods to avoid unnecessary repetition.
351
-
352
- Let's see how you can call methdos from your session and be
353
- able to execute GRASS commands from the method in the context of the session.
354
-
355
- Inside a session, `self` refers to an object of class
356
- `GrassGis::Context` which represents the current GRASS session.
357
-
358
- You can invoke grass commands directly on this object, so, if you pass
359
- this object around you can use it to execute GRASS commands:
360
-
361
- ```ruby
362
- def helper_method(grass)
363
- # ...
364
- end
365
-
366
- GrassGis.session configuration do
367
- helper_method self
368
- end
369
- ```
370
-
371
- In the helper method you can use the grass object like this;
372
-
373
- ```ruby
374
- def helper_method(grass)
375
- # change the current region resolution
376
- grass.g.region res: 10
377
- end
378
- ```
379
-
380
- To avoid having to prepend each command with `grapss.` you can
381
- use the `session` method like this:
382
-
383
- ```ruby
384
- def helper_method(grass)
385
- grass.session do
386
- g.region res: 10
387
- g.region '-p'
388
- puts output
389
- end
390
- end
391
- ```
392
-
393
- ### Examples
394
-
395
-
396
- #### 1. Map existence
397
-
398
- Helper methods to check for the existence of maps.
399
-
400
- Often we may want to know if a map exists. Next methods can be used to
401
- check for it.
402
-
403
- ```ruby
404
- def map_exists?(grass, type, map)
405
- grass.g.list type
406
- maps = grass.output.split
407
- maps.include?(map)
408
- end
409
-
410
- def raster_exists?(grass, map)
411
- map_exists? grass, 'rast', map
412
- end
413
-
414
- def vector_exists?(grass, map)
415
- map_exists? grass, 'vect', map
416
- end
417
- ```
418
-
419
- We can use these methods like this:
420
-
421
- ```ruby
422
- GrassGis.session configuration do
423
- unless raster_exists?(self, 'product')
424
- r.mapcalc "product = factor1*factor2"
425
- end
426
- end
427
- ```
428
-
429
- #### 2. Information as Hashes
430
-
431
- Following methods show how to obtain information about a raster map
432
- and the current region as a Hash:
433
-
434
- ```ruby
435
- def raster_info(grass, map)
436
- grass.r.info '-g', map
437
- shell_to_hash grass
438
- end
439
-
440
- def region_info(grass)
441
- grass.g.region '-m'
442
- shell_to_hash grass
443
- end
444
-
445
- def shell_to_hash(grass)
446
- Hash[grass.output.lines.map{|l| l.strip.split('=')}]
447
- end
448
-
449
- # Now, for example, we can easily obtain the resolution of a raster:
450
-
451
- def raster_res(grass, map)
452
- info = raster_info(grass, map)
453
- info.values_at('ewres', 'nsres').map(&:to_i)
454
- end
455
-
456
- def region_res(grass)
457
- info = region_info(grass)
458
- info.values_at('ewres', 'nsres').map(&:to_i)
459
- end
460
- ```
461
-
462
- #### 3. Average angle
463
-
464
- Let's assume we have a raster map `aspect` which is
465
- a direction angle (i.e. a cyclic value from 0 to 360).
466
-
467
- Now imagine that we need to compute a coarser raster grid with
468
- average values per cell. We can't just resample the angle
469
- (we want the average of 359 and 1 be 0, not 180);
470
- we would need an unitary vector or complex number to take averages.
471
-
472
- The next method will perform the average correctly using
473
- auxiliary raster maps for two cartesian components (that represent
474
- the angle as a vector).
475
-
476
- ```ruby
477
- def resample_average_angle(grass, options = {})
478
- input_raster = options[:input]
479
- raise "Raster #{input_raster} not found" unless raster_exists?(grass, input_raster)
480
- input_res = raster_res(grass, input_raster)
481
-
482
- if options[:output_res]
483
- output_res = options[:output_res]
484
- unless output_res.is_a?(Array)
485
- output_res = [output_res, output_res]
486
- end
487
- else
488
- output_res = region_res(grass)
489
- end
490
-
491
- output_raster = options[:output]
492
-
493
- grass.session do
494
- unless raster_exists?(self, "#{input_raster}_sin")
495
- g.region ewres: input_res[0], nsres: input_res[1]
496
- r.mapcalc "#{input_raster}_sin = sin(#{input_raster})"
497
- end
498
- unless raster_exists?(self, "#{input_raster}_cos")
499
- g.region ewres: input_res[0], nsres: input_res[1]
500
- r.mapcalc "#{input_raster}_cos = cos(#{input_raster})"
501
- end
502
- g.region ewres: output_res[0], nsres: output_res[1]
503
- r.resamp.stats input: "#{input_raster}_cos", output: "#{output_raster}_cos"
504
- r.resamp.stats input: "#{input_raster}_sin", output: "#{output_raster}_sin"
505
- r.mapcalc "#{output_raster} = atan(#{output_raster}_cos,#{output_raster}_sin)"
506
- r.colors map: ouput_raster, raster: input_raster
507
- g.remove '-f', type: 'raster', name: ["#{output_raster}_cos", "#{output_raster}_sin"]
508
- g.remove -f type=raster name=aspect_sin@landscape,aspect_cos@landscape
509
- end
510
- end
511
- ```
512
-
513
- Now, to resample a (cyclic angular) map `aspect_hires` to a lower resolution 10:
514
-
515
- ```ruby
516
- GrassGis.session configuration do
517
- resamp_average self,
518
- input: 'aspect_hires',
519
- output: 'aspect_lowres', output_res: 10
520
- end
521
- end
522
- ```
523
-
524
- ## Roadmap
525
-
526
- * Change Module to define explicitly available GRASS commands instead of
527
- accepting anything with `method_missing`. Declare commands with permitted
528
- arguments and options, etc.
529
- * Add some session helpers:
530
- - Method to clean GRASS temporaries ($GISBASE/etc/clean_temp), or do
531
- it automatically when disposing the session.
532
- - Methods to check if maps exist
533
- - Methods that return information as objects (arrays, hashes), e.g.
534
- values returned by r.what, the current region, etc.
535
- - Methods that execute operations in a GRASS-version independent
536
- manner (higher level, version independent interface to modules).
537
-
538
- ### GRASS cooking DSL
539
-
540
- Create a DSL to define GRASS processing recipes.
541
-
542
- It should handle errors gracefully during recipe execution,
543
- take care of logging, parsing command output, temporary files, etc.
544
-
545
- A recipe could have requirements such as which other recipes it depends
546
- upon (so must be executed first), which map layers must exist, etc.
547
-
548
- It could also define what map layers or other files are generated by the
549
- recipe.
550
-
551
- Example of intended recipe syntax:
552
-
553
- ```ruby
554
- recipe :resamp_average do |options = {}|
555
- # ...
556
- end
557
-
558
- recipe :generate_working_dem do |...|
559
- define :working_dem, 'dem'
560
- define :working_slope, 'slope'
561
- define :working_aspect, 'aspect'
562
- g.region res: working_res
563
- apply :resamp_average, input: base_dem, output: working_dem, input_res: base_dem_res, output_res: working_res
564
- apply :resamp_average, input: base_slope, output: working_slope, input_res: base_dem_res, output_res: working_res
565
- apply :resamp_average, input: base_aspect, output: working_aspect, input_res: base_dem_res, output_res: working_res, direction: true
566
- describe working_slope, "Pendiente en grados a #{working_res} #{region_units}"
567
- # ...
568
- end
569
- ```
570
-
571
- ## Contributing
572
-
573
- 1. Fork it ( https://github.com/[my-github-username]/grassgis/fork )
574
- 2. Create your feature branch (`git checkout -b my-new-feature`)
575
- 3. Commit your changes (`git commit -am 'Add some feature'`)
576
- 4. Push to the branch (`git push origin my-new-feature`)
577
- 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
+ - [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