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.
data/Rakefile CHANGED
@@ -1,22 +1,22 @@
1
- require "bundler/gem_tasks"
2
-
3
- require 'rake/testtask'
4
- Rake::TestTask.new(:test) do |test|
5
- test.libs << 'lib' << 'test'
6
- test.pattern = 'test/**/test_*.rb'
7
- test.verbose = true
8
- end
9
-
10
- require 'rdoc/task'
11
- Rake::RDocTask.new do |rdoc|
12
- version = GrassGis::VERSION
13
-
14
- rdoc.rdoc_dir = 'rdoc'
15
- rdoc.title = "GrassGis #{version}"
16
- rdoc.main = "README.md"
17
- rdoc.rdoc_files.include('README*')
18
- rdoc.rdoc_files.include('lib/**/*.rb')
19
- rdoc.markup = 'markdown' if rdoc.respond_to?(:markup)
20
- end
21
-
22
- task :default => :test
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/test_*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ require 'rdoc/task'
11
+ Rake::RDocTask.new do |rdoc|
12
+ version = GrassGis::VERSION
13
+
14
+ rdoc.rdoc_dir = 'rdoc'
15
+ rdoc.title = "GrassGis #{version}"
16
+ rdoc.main = "README.md"
17
+ rdoc.rdoc_files.include('README*')
18
+ rdoc.rdoc_files.include('lib/**/*.rb')
19
+ rdoc.markup = 'markdown' if rdoc.respond_to?(:markup)
20
+ end
21
+
22
+ task :default => :test
@@ -1,18 +1,19 @@
1
- # -*- mode: ruby -*-
2
- # vi: set ft=ruby :
3
-
4
- Vagrant.configure(2) do |config|
5
- config.vm.box = "ubuntu/trusty32"
6
-
7
- config.vm.provision "shell", inline: <<-SHELL
8
- sudo add-apt-repository -y ppa:ubuntugis/ubuntugis-unstable
9
- sudo add-apt-repository -y ppa:grass/grass-stable
10
- sudo apt-get update -y
11
- sudo apt-get install -y grass7
12
- sudo apt-get install -y grass7-dev
13
- sudo apt-get install -y build-essential
14
- sudo apt-get install -y ruby-dev
15
- sudo apt-get install -y libsqlite3-dev
16
- sudo gem install bundler
17
- SHELL
18
- end
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ # GRASS 7 environment on Ubuntu for tests
5
+ Vagrant.configure(2) do |config|
6
+ config.vm.box = "ubuntu/trusty32"
7
+
8
+ config.vm.provision "shell", inline: <<-SHELL
9
+ sudo add-apt-repository -y ppa:ubuntugis/ubuntugis-unstable
10
+ sudo add-apt-repository -y ppa:grass/grass-stable
11
+ sudo apt-get update -y
12
+ sudo apt-get install -y grass7
13
+ sudo apt-get install -y grass7-dev
14
+ sudo apt-get install -y build-essential
15
+ sudo apt-get install -y ruby-dev
16
+ sudo apt-get install -y libsqlite3-dev
17
+ sudo gem install bundler
18
+ SHELL
19
+ end
@@ -1,28 +1,28 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'grassgis/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "grassgis"
8
- spec.version = GrassGis::VERSION
9
- spec.authors = ["Javier Goizueta"]
10
- spec.email = ["jgoizueta@gmail.com"]
11
- spec.summary = %q{Support for scripting GRASS GIS in Ruby}
12
- spec.description = %q{Support for scripting GRASS GIS in Ruby.}
13
- spec.homepage = "https://github.com/jgoizueta/grassgis"
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_dependency "sys_cmd", "~> 1.1"
22
-
23
- spec.add_development_dependency "bundler", "~> 1.9"
24
- spec.add_development_dependency "rake"
25
- spec.add_development_dependency "minitest", "~> 5.4"
26
-
27
- spec.required_ruby_version = '>= 1.9.3'
28
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'grassgis/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "grassgis"
8
+ spec.version = GrassGis::VERSION
9
+ spec.authors = ["Javier Goizueta"]
10
+ spec.email = ["jgoizueta@gmail.com"]
11
+ spec.summary = %q{Support for scripting GRASS GIS in Ruby}
12
+ spec.description = %q{Support for scripting GRASS GIS in Ruby.}
13
+ spec.homepage = "https://github.com/jgoizueta/grassgis"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "sys_cmd", "~> 1.1"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.9"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "minitest", "~> 5.4"
26
+
27
+ spec.required_ruby_version = '>= 1.9.3'
28
+ end
@@ -1,8 +1,10 @@
1
- require 'sys_cmd'
2
- require 'grassgis/version'
3
- require 'grassgis/error'
4
- require 'grassgis/support'
5
- require 'grassgis/module'
6
- require 'grassgis/location'
7
- require 'grassgis/mapset'
8
- require 'grassgis/context'
1
+ require 'sys_cmd'
2
+ require 'grassgis/version'
3
+ require 'grassgis/error'
4
+ require 'grassgis/support'
5
+ require 'grassgis/tools'
6
+ require 'grassgis/module'
7
+ require 'grassgis/location'
8
+ require 'grassgis/mapset'
9
+ require 'grassgis/context'
10
+ require 'grassgis/cookbook'
@@ -30,6 +30,7 @@ module GrassGis
30
30
  config[:echo] = :commands unless config.key?(:echo)
31
31
 
32
32
  @config = config
33
+ @tools = (config[:tools] == false) ? false : true
33
34
 
34
35
  locals = config[:locals] || {}
35
36
  locals.each do |var_name, value|
@@ -119,6 +120,7 @@ module GrassGis
119
120
  insert_path 'PATH', *paths
120
121
  insert_path 'MANPATH', File.join(@config[:gisbase], 'man')
121
122
  @history = []
123
+ extend GrassGis::Tools if @tools
122
124
  end
123
125
 
124
126
  def dispose
@@ -0,0 +1,483 @@
1
+ # Recipes can only be defined when input parameters, files and maps are specific
2
+ # (although an input file can be a directory with an indetermninate number of files inside)
3
+ # and there's no dependency between input parameters/files/maps and which products are
4
+ # generated. (so the generated products are also specific, except, again, for the contents
5
+ # of directories).
6
+ # Also, recipes shouldn't generate any non-declared maps, i.e. temporary or
7
+ # auxiliary maps should be removed before the recipe ends.
8
+ #
9
+ # Temporary maps & files support, e.g.: temporaries are declared,
10
+ # then recipe is executed in a block with a ensure which removes temporary maps
11
+ #
12
+ # Example:
13
+ #
14
+ # GrassCookbook.recipe :dem_base_from_mdt05 do
15
+ # description %{
16
+ # Generate a DEM for the interest area at fixed 5m resolution
17
+ # from CNIG's MDT05 data.
18
+ # }
19
+ #
20
+ # required_files 'data/MDT05'
21
+ # generated_raster_maps 'dem_base'
22
+ #
23
+ # process do |mdt05_sheets|
24
+ # ...
25
+ # end
26
+ # end
27
+ #
28
+ # GrassCookbook.recipe :dem_base_derived do
29
+ # description %{
30
+ # Generate DEM-derived maps from the 5m dem base:
31
+ # slope, aspect and relief shading
32
+ # }
33
+ #
34
+ # required_raster_maps 'dem_base'
35
+ # generated_raster_maps 'shade_base', 'slope_base', 'aspect_base'
36
+ #
37
+ # process do
38
+ # r.relief input: 'dem_base', output: 'shade_base'
39
+ # r.slope.aspect elevation: 'dem_base',
40
+ # slope: 'slope_base',
41
+ # aspect: 'aspect_base'
42
+ # end
43
+ # end
44
+ #
45
+ # GrassCookbook.recipe :working_dem do
46
+ # description %{
47
+ # Generate DEM data at working resolution
48
+ # }
49
+ #
50
+ # required_raster_maps 'dem_base', 'slope_base', 'aspect_base'
51
+ # generated_raster_maps 'dem', 'slope', 'aspect'
52
+ #
53
+ # process do |resolution|
54
+ # ...
55
+ # end
56
+ # end
57
+ #
58
+ # # Now use our recipes to compute some permanent base maps and
59
+ # # alternative scenario mapsheets varying some parameter.
60
+ #
61
+ #
62
+ # GrassGis.session grass_config do
63
+ # # First generate maps using fixed parameters and move them to PERMANENT
64
+ # fixed_parameters = { mdt05_sheets: %w(0244 0282) }
65
+ # fixed_data = primary = GrassCookbook::Data[
66
+ # parameters: fixed_parameters.keys,
67
+ # files: GrassCookbook.existing_external_input_files
68
+ # ]
69
+ # plan = GrassCookbook.plan(fixed_data)
70
+ # permanent = plan.last
71
+ # GrassCookbook.replace_existing_products self, plan
72
+ # GrassCookbook.execute self, fixed_parameters, plan
73
+ # permanent.maps.each do |map, type|
74
+ # move_map(map, type: type, to: 'PERMANENT')
75
+ # end
76
+ #
77
+ # # Then define some variations of other parameters and create a mapset
78
+ # # for each variation, where maps dependent on the varying parameters
79
+ # # will be put
80
+ # variants = {
81
+ # '10m' => { resolucion: 10 },
82
+ # '25m' => { resolucion: 25 }
83
+ # }
84
+ # for variant_name, variant_parameters in variants
85
+ # data = GrassCookbook::Data[parameters: variant_parameters.keys] + permanent
86
+ # plan = GrassCookbook.plan(data)
87
+ # GrassCookbook.replace_existing_products self, plan
88
+ # GrassCookbook.execute self, fixed_parameters.merge(variant_parameters), plan
89
+ # variant_maps = (plan.last - data).maps
90
+ # create_mapset variant_name
91
+ # variant_maps.each do |map, type|
92
+ # move_map(map, type: type, to: variant_name)
93
+ # end
94
+ # end
95
+ # end
96
+ #
97
+ #
98
+ module GrassCookbook
99
+
100
+ # Datasets used by recipes, consist of parmeters, maps and files
101
+ # (or directories). Recipes use them to define both required and
102
+ # generated data.
103
+ class Data
104
+ def initialize(params = {})
105
+ @parameters = params[:parameters] || []
106
+ @files = params[:files] || []
107
+ @maps = params[:maps] || []
108
+ end
109
+
110
+ attr_reader :parameters, :files, :maps
111
+
112
+ def vector_maps
113
+ @maps.select { |m, t| t == :vector }.map(&:first)
114
+ end
115
+
116
+ def raster_maps
117
+ @maps.select { |m, t| t == :raster }
118
+ end
119
+
120
+ def merge!(data)
121
+ data = Data[data]
122
+ @parameters = (@parameters + data.parameters).uniq
123
+ @maps = (@maps + data.maps).uniq
124
+ @files = (@files + data.files).uniq
125
+ self
126
+ end
127
+
128
+ def dup
129
+ Data.new.merge! self
130
+ end
131
+
132
+ def self.[](params)
133
+ unless params.is_a?(Data)
134
+ params = Data.new(params)
135
+ end
136
+ params
137
+ end
138
+
139
+ def present?
140
+ @parameters.size > 0 || @files.size > 0 || @maps.size > 0
141
+ end
142
+
143
+ def empty?
144
+ !present?
145
+ end
146
+
147
+ def -(other)
148
+ other = Data[other]
149
+ Data[
150
+ parameters: parameters - other.parameters,
151
+ files: files - other.files,
152
+ maps: maps - other.maps
153
+ ]
154
+ end
155
+
156
+ def +(other)
157
+ dup.merge! other
158
+ end
159
+
160
+ # data which is requiered but not provided here
161
+ def missing(required)
162
+ Data[required] - self
163
+ end
164
+
165
+ def to_s
166
+ txt = "Datos:\n"
167
+ txt << " Parametros: #{parameters.inspect}\n"
168
+ txt << " Archivos: #{files.inspect}\n"
169
+ txt << " Mapas: #{maps.inspect}\n"
170
+ txt
171
+ end
172
+ end
173
+
174
+ @recipes = {}
175
+
176
+ # A recipe uses some Data and generates other Data in
177
+ # a GRASS session.
178
+ class Recipe
179
+ def initialize(options = {}, &blk)
180
+ @id = options[:id].to_sym
181
+ @description = options[:description] || "Proceso #{@id}"
182
+ @process = blk
183
+ @required_parameters = blk.parameters.map(&:last)
184
+ @required_raster_maps = Array(options[:required_raster_maps])
185
+ @required_vector_maps = Array(options[:required_vector_maps])
186
+ @required_files = Array(options[:required_files])
187
+ @generated_vector_maps = Array(options[:generated_vector_maps])
188
+ @generated_raster_maps = Array(options[:generated_raster_maps])
189
+ @generated_files = Array(options[:generated_files])
190
+ @generated_parameters = Array(options[:generated_parameters])
191
+ end
192
+
193
+ attr_reader :id, :description
194
+ attr_reader :required_raster_maps, :required_vector_maps, :required_files
195
+ attr_reader :required_parameters
196
+ attr_reader :generated_raster_maps, :generated_vector_maps, :generated_files
197
+ attr_reader :generated_parameters
198
+
199
+ # inputs
200
+ def requirements
201
+ Data[parameters: required_parameters, files: required_files, maps: required_maps]
202
+ end
203
+
204
+ # outputs
205
+ def products
206
+ Data[parameters: generated_parameters, files: generated_files, maps: generated_maps]
207
+ end
208
+
209
+ def required_maps
210
+ required_raster_maps.map { |map| [map, :raster] } +
211
+ required_vector_maps.map { |map| [map, :vector] }
212
+ end
213
+
214
+ def generated_maps
215
+ generated_raster_maps.map { |map| [map, :raster] } +
216
+ generated_vector_maps.map { |map| [map, :vector] }
217
+ end
218
+
219
+ # Can the recipe be done given provided input?
220
+ def doable?(input)
221
+ # input.missing(requirements).empty?
222
+ (requirements - input).empty?
223
+ end
224
+
225
+ # Is the recipe unnecessary given existing data?
226
+ def done?(existing)
227
+ (products - existing).empty?
228
+ end
229
+
230
+ # Execute the recipe with given parameters # TODO: mapset PATH
231
+ def cook(grass, parameters = {})
232
+ # grass.g.mapsets '-p'
233
+ # current_mapset = output.lines.last.split.first
234
+
235
+ # Leave planning/checking if doable to the caller; not done here:
236
+ # unless doable? GrassCookbook.available_data(grass, parameters)
237
+ # raise "Requirements for #{@id} not fulfilled"
238
+ # end
239
+
240
+ # Also not done here:
241
+ # if done? GrassCookbook.available_data(grass, parameters)
242
+ # return
243
+ # end
244
+
245
+ args = parameters.values_at(*@required_parameters)
246
+ # @process.call grass, *args
247
+ # grass.session &@process, arguments: args
248
+ # TODO: support in GrassGis for injecting/replacing locals
249
+ grass.define_singleton_method(:parameters) { @parameters }
250
+ grass.instance_exec *args, &@process
251
+ end
252
+
253
+ def to_s
254
+ @description
255
+ end
256
+
257
+ def inspect
258
+ "<GrassCookbook::Recipe #{@id.inspect}>"
259
+ end
260
+
261
+ def eql?(other)
262
+ @id == other.id
263
+ end
264
+
265
+ def ==(other)
266
+ eql? other
267
+ end
268
+
269
+ def hash
270
+ @id.hash
271
+ end
272
+ end
273
+
274
+ # DSL to define recipes
275
+ class RecipeDsl
276
+ def initialize(id)
277
+ @id = id.to_sym
278
+ @required_raster_maps = []
279
+ @required_vector_maps = []
280
+ @required_files = []
281
+ @generated_raster_maps = []
282
+ @generated_vector_maps = []
283
+ @generated_files = []
284
+ @generated_parameters = []
285
+ @description = nil
286
+ end
287
+
288
+ def description(text)
289
+ @description = GrassGis::Support.unindent(text)
290
+ end
291
+
292
+ def required_raster_maps(*maps)
293
+ @required_raster_maps += maps
294
+ end
295
+
296
+ def required_vector_maps(*maps)
297
+ @required_vector_maps += maps
298
+ end
299
+
300
+ def required_files(*files)
301
+ @required_files += files
302
+ end
303
+
304
+ def generated_vector_maps(*maps)
305
+ @generated_vector_maps += maps
306
+ end
307
+
308
+ def generated_raster_maps(*maps)
309
+ @generated_raster_maps += maps
310
+ end
311
+
312
+ def generated_files(*files)
313
+ @generated_files += files
314
+ end
315
+
316
+ def generated_parameters(*parameters)
317
+ @generated_parameters += parameters
318
+ end
319
+
320
+ def process(&blk)
321
+ @process = blk
322
+ end
323
+
324
+ def recipe
325
+ Recipe.new(
326
+ id: @id,
327
+ required_maps: @required_maps,
328
+ required_files: @required_files,
329
+ generated_raster_maps: @generated_raster_maps,
330
+ generated_vector_maps: @generated_vector_maps,
331
+ generated_files: @generated_files,
332
+ generated_parameters: @generated_parameters,
333
+ &@process
334
+ )
335
+ end
336
+ end
337
+
338
+ class <<self
339
+ def recipe(id, &blk)
340
+ dsl = RecipeDsl.new(id)
341
+ dsl.instance_eval &blk
342
+ @recipes[id.to_sym] = dsl.recipe
343
+ end
344
+
345
+ def [](recipe)
346
+ if recipe.is_a? Recipe
347
+ recipe
348
+ else
349
+ @recipes[recipe.to_sym]
350
+ end
351
+ end
352
+
353
+ def cook(grass, recipe, parameters)
354
+ grass.log "Recipe: #{recipe}"
355
+ self[recipe].cook grass, parameters
356
+ end
357
+
358
+ def all_files_used
359
+ @recipes.values.map(&:required_files).flatten.uniq
360
+ end
361
+
362
+ def all_maps_used
363
+ @recipes.values.map(&:required_maps).flatten(1).uniq
364
+ end
365
+
366
+ def all_files_possible
367
+ @recipes.values.map(&:generated_files).flatten.uniq
368
+ end
369
+
370
+ def all_maps_possible
371
+ @recipes.values.map(&:generated_maps).flatten(1).uniq
372
+ end
373
+
374
+ def existing_input_files
375
+ all_files_used.select { |f| File.exists?(f) }
376
+ end
377
+
378
+ def existing_input_maps(grass)
379
+ # TODO: use mapset PATH here (add as another parameters)
380
+ path = []
381
+ all_maps_used.select { |m, t| grass.map_exists?(m, type: t, mapset: path) }
382
+ end
383
+
384
+ # Generate ordered recipes and generated products (output)
385
+ # than can be obtained with available inputdata.
386
+ def plan(input_data)
387
+ input = Data[input_data]
388
+ existing = input.dup
389
+
390
+ applied_recipes = []
391
+
392
+ remaining_recipes = @recipes.values - applied_recipes
393
+
394
+ while remaining_recipes.size > 0
395
+ progress = false
396
+ remaining_recipes.each do |recipe|
397
+ unless recipe.done?(existing)
398
+ if recipe.doable?(existing)
399
+ progress = true
400
+ applied_recipes << recipe
401
+ existing.merge! recipe.products
402
+ end
403
+ end
404
+ end
405
+ break unless progress
406
+ remaining_recipes -= applied_recipes
407
+ end
408
+ [applied_recipes, existing - input]
409
+ end
410
+
411
+ def available_data(grass, parameters)
412
+ Data[
413
+ parameters: parameters,
414
+ files: existing_input_files,
415
+ maps: existing_input_maps(grass)
416
+ ]
417
+ end
418
+
419
+ def achievable_results(grass, parameters)
420
+ inputs = available_data(grass, parameters)
421
+ inputs + plan(inputs).last
422
+ end
423
+
424
+ # primary input files
425
+ # input files that exist (and are not generated, so they are externally provided input)
426
+ def existing_external_input_files
427
+ existing_input_files - all_files_possible
428
+ end
429
+
430
+ def permantent_results(grass, fixed_parameters)
431
+ primary = Data[parameters: fixed_parameters, files: existing_external_input_files]
432
+ plan(primary).last
433
+ end
434
+
435
+ def missing_input(grass)
436
+ Data[
437
+ files: all_files_used - existing_input_files,
438
+ maps: all_maps_used - existing_input_maps(grass)
439
+ ]
440
+ end
441
+
442
+ def impossible_results(grass, parameters)
443
+ possibilities = Data[
444
+ files: all_files_possible,
445
+ maps: all_maps_possible
446
+ ]
447
+ possibilities - achievable_results(grass, parameters)
448
+ end
449
+
450
+ def execute(grass, parameters, plan)
451
+ recipes, result = plan
452
+ recipes.each do |recipe|
453
+ cook grass, recipe, parameters
454
+ end
455
+ result
456
+ end
457
+
458
+ def replace_existing_products(grass, plan, parameters = nil)
459
+ results = plan.last
460
+ if parameters
461
+ results.parameters.each do |parameter|
462
+ parameters.delete parameter
463
+ end
464
+ end
465
+ results.maps.each do |map, type|
466
+ mapset = grass.explicit_map_mapset(map) || grass.current_mapset
467
+ if grass.map_exists?(map, type: type, mapset: mapset)
468
+ grass.remove_map(map, type: type, mapset: mapset)
469
+ end
470
+ end
471
+ results.files.each do |file|
472
+ if File.exists?(file)
473
+ if File.directory?(file)
474
+ FileUtils.rm_rf file
475
+ else
476
+ FileUtils.rm file
477
+ end
478
+ end
479
+ end
480
+ end
481
+
482
+ end
483
+ end