grassgis 0.3.0 → 0.3.1

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: 299ce1dcdebe07da42c2207549ab9fdeacc9cb2b
4
- data.tar.gz: 206df84d976c27567c4a5fe7fc3dd1949d5f92d6
3
+ metadata.gz: c0fd89fb91b60e769c7f1a29f2a683525a162480
4
+ data.tar.gz: ab57d26849db6fdbada05be7803b3c67df94f0ff
5
5
  SHA512:
6
- metadata.gz: be5e141c19ebc4880733387277d61a47fca4993ba1b74e57de8461c52b93965d83d3380864acc3aa37ac9886c53674d463b098ee0b193f2b7742c843c49a845d
7
- data.tar.gz: f9d883babf14d38607cc7430c964cfd10d017a246256b135ef6a26a6ba65db142481c94700744d975c081047506305aca059050870bb164355e8fc744ece2b29
6
+ metadata.gz: b35d3856bc5b77a5fd8eb104db5cddb113140841e59b840bfa99dc74625fa4cc3e97efbce586536b8005756b5ef4d894121d35f5b092258620c702d4e78c12fd
7
+ data.tar.gz: 4cd8093c0acb3c25556e1cefc01f0642c9220f76a7e17c3fe7c2b60a034904519d821b090bfc9ecb9f80196721bb8a87a6a5707dda20f3533115eb3cf65c064a
data/README.md CHANGED
@@ -24,26 +24,361 @@ Or install it yourself as:
24
24
  This library can prepare environments to execute GRASS commands
25
25
  from Ruby scripts.
26
26
 
27
- Example:
27
+ First we require the library:
28
28
 
29
29
  ```ruby
30
30
  require 'grassgis'
31
+ ```
32
+
33
+ ### Configuration
34
+
35
+ A GRASS session operates on a given location and mapset.
31
36
 
37
+ Before starting a GRASS session we need some configuration
38
+ parameters to specify the location of the GRASS Installation
39
+ to be used and the location/mapset. We do this by using
40
+ a Ruby Hash containing configuration parameters:
41
+
42
+ ```ruby
32
43
  configuration = {
33
- gisbase: '/usr/local/Cellar/grass-70/7.0.0/grass-7.0.0',
34
- location: 'world'
44
+ gisbase: '/usr/local/grass-7.0.0',
45
+ gisdbase: File.join(ENV['HOME'], 'grassdata'),
46
+ location: 'nc_spm',
47
+ mapset: 'user1'
35
48
  }
49
+ ```
50
+
51
+ So, you first need to know where is GRASS installed on your system
52
+ to define the `:gisbase` option to point to the base directory of the GRASS
53
+ installation.
54
+
55
+ In *Windows*, if installed with
56
+ [OSGeo4W](http://trac.osgeo.org/osgeo4w/) it is typically of
57
+ the form `C:\OGeo4W\app\grass\grass-7.0.0` (the last directory will vary
58
+ depending on the GRASS version).
59
+
60
+ Under *Mac OS X*, if using [Homebrew](http://brew.sh/)
61
+ (with the [osgeo/osgeo4mac](https://github.com/OSGeo/homebrew-osgeo4mac) tap)
62
+ it should bd something like `/usr/local/Cellar/grass-70/7.0.0/grass-7.0.0`.
63
+
64
+ You can find the `:gisbase` directory by executing the
65
+ `grass` command of your system (which may be `grass70`, `grass64`, etc.)
66
+ with the `--config path` option:
67
+
68
+ grass --config path
36
69
 
70
+ You must also specify the `GISDBASE`, `LOCATION` and `MAPSET`, to work with,
71
+ just like when starting GRASS, through the `:gisdbase`, `:location` and
72
+ `:mapset` configuration options.
73
+ You can omit `:gisdbase` which will default to a directory named `grassdata` in the
74
+ user's home directory and `:mapset` which defaults to `PERMANENT`.
75
+
76
+
77
+ ### Running a GRASS Session
78
+
79
+ With the proper configuration in place, we can use it to
80
+ create a GRASS Session and execute GRASS command from it:
81
+
82
+ ```ruby
37
83
  GrassGis.session configuration do
38
- r.resamp.stats '-n', input: "map1@mapset1", output: "map2"
39
84
  g.list 'vect'
40
- puts output # output of last command
85
+ puts output # will print list of vector maps
86
+ end
87
+ ```
88
+
89
+ Inside a `GrassGis` session we can execute GRASS commands
90
+ just by using the command name as a Ruby method.
91
+
92
+ Command flags and options must be passed first as regular method arguments,
93
+ then named parameters must be passed as a Ruby Hash:
94
+
95
+ ```ruby
96
+ g.region '-p', rast: 'elevation'
97
+ d.rast 'elevation'
98
+ d.vect 'streams', col: 'blue'
99
+ ```
100
+
101
+ If you try to execute an invalid module name an `ENOENT`
102
+ error will be raised:
103
+
104
+ ```ruby
105
+ g.this.module.does.not.exist '???'
106
+ ```
107
+
108
+ If the command to be executed has no arguments you need
109
+ to invoke `.run` on it to execute it:
110
+
111
+ ```ruby
112
+ d.erase.run
113
+ g.list.run
114
+ ```
115
+
116
+ ## Helper methods
117
+
118
+ When writing a non-trivial program you'll probably
119
+ find you want to define methods to avoid innecesary repetition.
120
+
121
+ Let's see how you can call methdos from your session and be
122
+ able to execute GRASS commands from the method in the context of the session.
123
+
124
+ Inside a session, `self` refers to an object of class
125
+ `GrassGis::Context` which represents the current GRASS session.
126
+
127
+ You can invoke grass commands directly on this object, so, if you pass
128
+ this object aroung you can use it to execute GRASS commands:
129
+
130
+ ```ruby
131
+ def helper_method(grass)
132
+ # ...
133
+ end
134
+
135
+ GrassGis.session configuration do
136
+ helper_method self
41
137
  end
42
138
  ```
43
139
 
140
+ In the helper method you can use the grass object like this;
141
+
142
+ ```ruby
143
+ def helper_method(grass)
144
+ # change the current region resolution
145
+ grass.g.region res: 10
146
+ end
147
+ ```
148
+
149
+ To avoid having to prepend each command with `grapss.` you can
150
+ use the `session` method like this:
151
+
152
+ ```ruby
153
+ def helper_method(grass)
154
+ grass.session do
155
+ g.region res: 10
156
+ g.region '-p'
157
+ puts output
158
+ end
159
+ end
160
+ ```
161
+
162
+ ### Examples
163
+
164
+
165
+ #### 1. Map existence
166
+
167
+ Helper methods to check for the existence of maps.
168
+
169
+ Often we may want to know if a map exists. Next methods can be used to
170
+ check for it.
171
+
172
+ ```ruby
173
+ def map_exists?(grass, type, map)
174
+ grass.g.list type
175
+ maps = grass.output.split
176
+ maps.include?(map)
177
+ end
178
+
179
+ def raster_exists?(grass, map)
180
+ map_exists? grass, 'rast', map
181
+ end
182
+
183
+ def vector_exists?(grass, map)
184
+ map_exists? grass, 'vect', map
185
+ end
186
+ ```
187
+
188
+ We can use these methods like this:
189
+
190
+ ```ruby
191
+ GrassGis.session configuration do
192
+ unless raster_exists?(self, 'product')
193
+ r.mapcalc "product = factor1*factor2"
194
+ end
195
+ end
196
+ ```
197
+
198
+
199
+ ### 2. Information as Hashes
200
+
201
+ Following methods show how to obtain information about a raster map
202
+ and the current region as a Hash:
203
+
204
+ ```ruby
205
+ def raster_info(grass, map)
206
+ grass.r.info '-g', map
207
+ shell_to_hash grass
208
+ end
209
+
210
+ def region_info(grass)
211
+ grass.g.region '-m'
212
+ shell_to_hash grass
213
+ end
214
+
215
+ def shell_to_hash(grass)
216
+ Hash[grass.output.lines.map{|l| l.strip.split('=')}]
217
+ end
218
+
219
+ # Now, for example, we can easily obtain the resolution of a raster:
220
+
221
+ def raster_res(grass, map)
222
+ info = raster_info(grass, map)
223
+ info.values_at('ewres', 'nsres').map(&:to_i)
224
+ end
225
+
226
+ def region_res(grass)
227
+ info = region_info(grass)
228
+ info.values_at('ewres', 'nsres').map(&:to_i)
229
+ end
230
+ ```
231
+
232
+ ### 3. Average angle
233
+
234
+ Let's assume we have a raster map `aspect` which is
235
+ a direction angle (i.e. a cyclic value from 0 to 360).
236
+
237
+ Now imagine that we need to compute a coarser raster grid with
238
+ average values per cell. We can't just resample the angle
239
+ (we want the average of 359 and 1 be 0, not 180);
240
+ we would need an unitary vector or complex number to take averages.
241
+
242
+ The next method will perform the average correctly using
243
+ auxiliary raster maps for two cartesian components (that represent
244
+ the angle as a vector).
245
+
246
+ ```ruby
247
+ def resample_average_angle(grass, options = {})
248
+ input_raster = options[:input]
249
+ raise "Raster #{input_raster} not found" unless raster_exists?(grass, input_raster)
250
+ input_res = raster_res(grass, input_raster)
251
+
252
+ if options[:output_res]
253
+ output_res = options[:output_res]
254
+ unless output_res.is_a?(Array)
255
+ output_res = [output_res, output_res]
256
+ end
257
+ else
258
+ output_res = region_res(grass)
259
+ end
260
+
261
+ output_raster = options[:output]
262
+
263
+ grass.session do
264
+ unless raster_exists?(self, "#{input_raster}_sin")
265
+ g.region ewres: input_res[0], nsres: input_res[1]
266
+ r.mapcalc "#{input_raster}_sin = sin(#{input_raster})"
267
+ end
268
+ unless raster_exists?(self, "#{input_raster}_cos")
269
+ g.region ewres: input_res[0], nsres: input_res[1]
270
+ r.mapcalc "#{input_raster}_cos = cos(#{input_raster})"
271
+ end
272
+ g.region ewres: output_res[0], nsres: output_res[1]
273
+ r.resamp.stats input: "#{input_raster}_cos", output: "#{output_raster}_cos"
274
+ r.resamp.stats input: "#{input_raster}_sin", output: "#{output_raster}_sin"
275
+ r.mapcalc "#{output_raster} = atan(#{output_raster}_cos,#{output_raster}_sin)"
276
+ r.colors map: ouput_raster, raster: input_raster
277
+ g.remove '-f', type: 'raster', name: ["#{output_raster}_cos", "#{output_raster}_sin"]
278
+ g.remove -f type=raster name=aspect_sin@landscape,aspect_cos@landscape
279
+ end
280
+ end
281
+ ```
282
+
283
+ Now, to resample a (cyclic angular) map `aspect_hires` to a lower resolution 10:
284
+
285
+ ```ruby
286
+ GrassGis.session configuration do
287
+ resamp_average self,
288
+ input: 'aspect_hires',
289
+ output: 'aspect_lowres', output_res: 10
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### Options
295
+
296
+ TODO: Explain options for error handling, echoing, logging, ...
297
+
298
+ ### Technicalities
299
+
300
+ #### Session scopes
301
+
302
+ In a session block, the Ruby `self` object is altered to
303
+ refer to a `GrassGis::Context` object. That means that in addition
304
+ to the enclosing `self`, any instance variables of the enclosing
305
+ scope are not directly available. This may cause some surprises
306
+ but is easy to overcome.
307
+
308
+ ```ruby
309
+ @value = 10
310
+ GrassGis.session configuration do
311
+ puts @value # nil!
312
+ end
313
+ ```
314
+
315
+ A possible workaround is to assign instance variables that we need
316
+ in the session to local variables:
317
+
318
+ ```ruby
319
+ @value = 10
320
+ value = @value
321
+ GrassGis.session configuration do
322
+ puts value # 10
323
+ end
324
+ ```
325
+
326
+ To avoid defining these variables you can pass a `:locals` Hash
327
+ in the configuration to define values that you need to access
328
+ in the session (but you won't be able to assign to them, because
329
+ they're not local variables!)
330
+
331
+ ```ruby
332
+ @value = 10
333
+
334
+ GrassGis.session configuration.merge(locals: { value: @value }) do
335
+ puts value # 10
336
+ value = 11 # don't do this: you're creating a local in the session
337
+ end
338
+ ```
339
+
340
+ A different approach is prevent the session block from using a special
341
+ `self` by defining a parameter to the block. This parameter will have
342
+ the value of a `GrassGis::Context` which you'll need to explicitly use
343
+ to execute any commands:
344
+
345
+ ```ruby
346
+ @value = 10
347
+ GrassGis.session configuration do |grass|
348
+ puts @value # 10
349
+ grass.g.region res: 10 # now you need to use the object to issue commans
350
+ end
351
+ ```
352
+
353
+ #### Invalid commands
354
+
355
+ Currently the generation of GRASS commands inside a session is
356
+ implemented in a versy simple way which allows to generate any command
357
+ name even if it is invalid or does not exist. This has the advantage
358
+ of supporting any version of GRASS, but doesn't allow for early
359
+ detection of invalid commands (e.g. due to typos) or invalid command
360
+ parameters.
361
+
362
+ ```ruby
363
+ GrassGis.session configuration do |grass|
364
+ g.regoin res: 10 # Oops (runtime error)
365
+ g.anything.goes.run # another runtime error
366
+ end
367
+ ```
368
+
369
+ If the command generated does not exist a runtime `ENOENT` exception will
370
+ occur.
371
+
372
+ If the command exists, then if parameters are not valid, the command
373
+ will execute but will return an error status. This will be handled
374
+ as explaned above.
375
+
44
376
  ## Roadmap
45
377
 
46
- * Write more documentation with examples.
378
+ * Methods to create new locations and mapsets.
379
+ * Change Module to define explicitly available GRASS commands instead of
380
+ accepting anything with `method_missing`. Declare commands with permitted
381
+ arguments and options, etc.
47
382
  * Add some session helpers:
48
383
  - Method to clean GRASS temporaries ($GISBASE/etc/clean_temp), or do
49
384
  it automatically when disposing the session.
@@ -68,28 +403,7 @@ Example of intended recipe syntax:
68
403
 
69
404
  ```ruby
70
405
  recipe :resamp_average do |options = {}|
71
- input_raster = options[:input]
72
- input_res = options[:input_res] # TODO: extract from input_rater info
73
- output_raster = options[:output]
74
- output_res = options[:output_res] # TODO: extract from output_raster info
75
- if options[:direction]
76
- unless raster_exists?("#{input_raster}_sin")
77
- g.region res: input_res
78
- r.mapcalc "#{input_raster}_sin = sin(#{input_raster})"
79
- end
80
- unless raster_exists?("#{input_raster}_cos")
81
- g.region res: input_res
82
- r.mapcalc "#{input_raster}_cos = cos(#{input_raster})"
83
- end
84
- g.region res: output_res
85
- r.resamp.stats input: "#{input_raster}_cos", output: "#{output_raster}_cos"
86
- r.resamp.stats input: "#{input_raster}_sin", output: "#{output_raster}_sin"
87
- r.mapcalc "#{output_raster} = atan(#{output_raster}_cos,#{output_raster}_sin)"
88
- g.remove rast: ["#{output_raster}_cos", "#{output_raster}_sin"]
89
- else
90
- g.region res: output_res
91
- r.resamp.stats input: input_raster, output: output_raster
92
- end
406
+ # ...
93
407
  end
94
408
 
95
409
  recipe :generate_working_dem do |...|
@@ -161,7 +161,11 @@ module GrassGis
161
161
  # end
162
162
  #
163
163
  def session(&blk)
164
- instance_eval(&blk)
164
+ if blk.arity == 1
165
+ blk.call self
166
+ else
167
+ instance_eval &blk
168
+ end
165
169
  end
166
170
 
167
171
  def dry?
@@ -179,7 +183,7 @@ module GrassGis
179
183
  log log_file, cmd.to_s(with_input: true)
180
184
  end
181
185
  unless dry?
182
- cmd.run error_output: :reparate
186
+ cmd.run error_output: :separate
183
187
  end
184
188
  if cmd.output
185
189
  puts cmd.output if @config[:echo] == :output
@@ -278,7 +282,9 @@ module GrassGis
278
282
  # :errors to define the behaviour when a GRASS command fails:
279
283
  # * :raise is the default and raises on errors
280
284
  # * :console shows standar error output of commands
281
- # * :quiet error output is retained but not shown
285
+ # * :quiet error output is retained but not shown; no exceptions are
286
+ # raise except when the command cannot be executed (e.g. when
287
+ # the command name is ill-formed)
282
288
  #
283
289
  # If :errors is anything other than :raise, it is up to the user
284
290
  # to check each command for errors. With the :console option
@@ -297,6 +303,12 @@ module GrassGis
297
303
  # * :output show the output of commands too
298
304
  # * false don't echo anything
299
305
  #
306
+ # Testing/debugging options:
307
+ #
308
+ # * :dry prevents actual execution of any command
309
+ # * errors: :silent omits raising exceptions (as :quiet) even when
310
+ # a command cannot be executed (usually because of an invalid command name)
311
+ #
300
312
  def self.session(config, &blk)
301
313
  context = Context.new(config)
302
314
  context.allocate
@@ -326,10 +338,13 @@ module GrassGis
326
338
 
327
339
  def self.error(command, error_mode = :raise)
328
340
  if command
329
- if error_mode == :raise
330
- if command.error
331
- raise command.error
332
- elsif (command.status_value && command.status_value != 0)
341
+ if command.error # :silent mode for testing/debugging?
342
+ # Errors that prevent command execution
343
+ # (usually ENOENT because the command does not exist)
344
+ # are always raised
345
+ raise command.error unless error_mode == :silent
346
+ elsif error_mode == :raise
347
+ if (command.status_value && command.status_value != 0)
333
348
  raise Error.new, error_info(command)
334
349
  end
335
350
  end
@@ -1,3 +1,3 @@
1
1
  module GrassGis
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grassgis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Goizueta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-23 00:00:00.000000000 Z
11
+ date: 2015-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sys_cmd
@@ -105,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
105
  version: '0'
106
106
  requirements: []
107
107
  rubyforge_project:
108
- rubygems_version: 2.4.6
108
+ rubygems_version: 2.4.8
109
109
  signing_key:
110
110
  specification_version: 4
111
111
  summary: Support for scripting GRASS GIS in Ruby