grassgis 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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