geoptima 0.0.9 → 0.1.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/bin/csv_chart CHANGED
@@ -8,8 +8,10 @@ require 'geoptima/chart'
8
8
  require 'geoptima/version'
9
9
  require 'geoptima/options'
10
10
  require 'fileutils'
11
+ require 'geoptima/daterange'
11
12
 
12
- Geoptima::assert_version("0.0.9")
13
+ Geoptima::assert_version("0.1.0")
14
+ Geoptima::Chart.available? || puts("No charting libraries available") || exit(-1)
13
15
 
14
16
  $export_dir = '.'
15
17
  $diversity = 40.0
@@ -17,14 +19,22 @@ $diversity = 40.0
17
19
  $files = Geoptima::Options.process_args do |option|
18
20
  option.m {$merge_all = true}
19
21
  option.a {$create_all = true}
22
+ option.t {$time_split = true}
20
23
  option.D {$export_dir = ARGV.shift}
21
24
  option.N {$merged_name = ARGV.shift}
22
25
  option.S {$specfile = ARGV.shift}
23
26
  option.P {$diversity = ARGV.shift.to_f}
27
+ option.T do
28
+ $time_range = Geoptima::DateRange.new(*(ARGV.shift.split(/[\,]+/).map do |t|
29
+ DateTime.parse t
30
+ end))
31
+ end
24
32
  end
25
33
 
26
34
  FileUtils.mkdir_p $export_dir
27
35
 
36
+ $create_all = true unless($specfile)
37
+ $merge_all = true if($time_split)
28
38
  $help = true unless($files.length>0)
29
39
  if $help
30
40
  puts <<EOHELP
@@ -33,44 +43,46 @@ Usage: csv_chart <-dham> <-S specfile> <-N name> <-D dir> files...
33
43
  -h print this help #{cw $help}
34
44
  -a automatically create charts for all properties #{cw $create_all}
35
45
  -m merge all files into single stats #{cw $merge_all}
46
+ -t merge and split by time (days) #{cw $time_split}
36
47
  -N use specified name for merged dataset: #{$merged_name}
37
48
  -D export charts to specified directory: #{$export_dir}
38
49
  -S use chart specification in specified file: #{$specfile}
39
50
  -P diversity threshold in percentage for automatic reports: #{$diversity}
51
+ -T set time-range filter: #{$time_range}
40
52
  Files to import: #{$files.join(', ')}
41
53
  EOHELP
42
54
  exit
43
55
  end
44
56
 
45
57
  class Stats
46
- attr_reader :file, :name, :headers, :stats, :data
47
- def initialize(file,name,fields)
48
- @file = file
58
+ attr_reader :name, :stats, :data, :numerical
59
+ def initialize(name)
49
60
  @name = name
50
- @headers = fields
51
- @stats = fields.map{|h| {}}
52
- @data = fields.map{|h| []}
53
- @numerical = fields.map{|h| true}
54
- end
55
- def add_header(h)
56
- @headers << h
57
- @stats << {}
58
- @data << []
59
- @numerical << true
60
- @headers.length - 1
61
+ @stats = {}
62
+ @data = []
63
+ @numerical = true
61
64
  end
62
- def add(field,index)
63
- puts "\tAdding field '#{field}' at index #{index}" if($debug)
65
+ def add(field)
66
+ puts "\tAdding field '#{field}' for property #{name}" if($debug)
64
67
  if field && field =~ /\w/
65
- @numerical[index] &&= is_number?(field)
66
- puts "\tField[#{index}]: #{field}" if($debug)
67
- stats = @stats[index]
68
+ @numerical &&= is_number?(field)
68
69
  stats[field] ||= 0
69
70
  stats[field] += 1
70
- puts "\tField[#{index}]: #{field} => #{stats[field]}" if($debug)
71
- @data[index] << field
71
+ @data << field
72
72
  end
73
73
  end
74
+ def length
75
+ @stats.length
76
+ end
77
+ def numerical?
78
+ @numerical
79
+ end
80
+ def diversity
81
+ 100.0 * @stats.length.to_f / @data.length.to_f
82
+ end
83
+ def diverse?
84
+ @stats.length>500 || diversity > $diversity
85
+ end
74
86
  def is_number?(field)
75
87
  is_integer?(field) || is_float?(field)
76
88
  end
@@ -80,36 +92,104 @@ class Stats
80
92
  def is_float?(field)
81
93
  field.to_f.to_s == field
82
94
  end
83
- def length(index)
84
- @stats[index].length
95
+ def to_s
96
+ "#{name}[#{length}]"
97
+ end
98
+ end
99
+
100
+ class StatsManager
101
+ attr_reader :name, :headers, :stats
102
+ def initialize(name)
103
+ @name = name
104
+ @headers = []
105
+ @stats = {}
106
+ end
107
+ def time_index
108
+ @time_index ||= @headers.index('Time') || @headers.index('Timestamp')
109
+ end
110
+ def time_stats
111
+ @time_stats ||= get_stats('Time') || get_stats('Timestamp')
112
+ end
113
+ def set_headers(headers)
114
+ @headers = []
115
+ headers.each {|h| add_header(h)}
116
+ $specs && $specs.add_stats(self,headers)
117
+ end
118
+ def add_header(h)
119
+ if @headers.index(h)
120
+ puts "Stats header already exists: #{h}"
121
+ else
122
+ @headers << h
123
+ @stats[h] ||= Stats.new(h)
124
+ end
125
+ @headers.index(h)
126
+ end
127
+ def get_stats(header)
128
+ stats[header] || stats[header.downcase]
129
+ end
130
+ def add_all(fields,headers)
131
+ fields.each_with_index do |field,index|
132
+ add(field,headers[index])
133
+ end
134
+ $specs && $specs.add_fields(self,fields)
85
135
  end
86
- def diversity(index)
87
- 100.0 * @stats[index].length.to_f / @data[index].length.to_f
136
+ def add(field,header)
137
+ puts "\tAdding field '#{field}' for property #{header}" if($debug)
138
+ add_header(header) unless(@stats[header])
139
+ @stats[header].add(field)
88
140
  end
89
- def diverse?(index)
90
- @stats[index].length>500 || diversity(index) > $diversity
141
+ def length
142
+ @stats.length
91
143
  end
92
- def numerical?(index)
93
- @numerical[index]
144
+ def to_s
145
+ "Stats[#{length}]: #{@stats.inspect}"
94
146
  end
95
147
  end
96
148
 
97
- $stats = {}
98
-
99
149
  module Geoptima
100
150
  class StatSpec
101
- attr_reader :header, :index, :indices, :fields, :options, :proc
151
+ attr_reader :header, :index, :indices, :fields, :options, :proc, :groups
102
152
  def initialize(header,*fields,&block)
103
153
  @header = header
104
154
  @fields = fields
105
155
  @proc = block
156
+ @groups = {}
106
157
  if @fields[-1].is_a?(Hash)
107
158
  @options = @fields.pop
108
159
  else
109
160
  @options = {}
110
161
  end
162
+ if @options[:group]
163
+ case @options[:group].to_s.intern
164
+ when :months
165
+ group_by {|t| t.strftime("%Y-%m")}
166
+ when :days
167
+ group_by {|t| t.strftime("%Y-%m-%d")}
168
+ else
169
+ group_by {|t| t.strftime("%Y-%m-%d %H")}
170
+ end
171
+ end
111
172
  puts "Created StatSpec: #{self}"
112
173
  end
174
+ def group_by(&block)
175
+ @group = block
176
+ end
177
+ def add(stats_manager,fields)
178
+ if @group
179
+ begin
180
+ time = DateTime.parse(fields[stats_manager.time_index])
181
+ if $time_range.nil? || $time_range.include?(time)
182
+ key = @group.call(time)
183
+ ghead = "#{header} #{key}"
184
+ @groups[key] = ghead
185
+ stats_manager.add(map(fields),ghead)
186
+ end
187
+ rescue ArgumentError
188
+ puts "Error: Unable to process time field[#{time}]: #{$!}"
189
+ end
190
+ end
191
+ stats_manager.add(map(fields),header)
192
+ end
113
193
  def mk_range(val)
114
194
  if val =~ /\w/
115
195
  div = options[:div].to_i
@@ -120,7 +200,7 @@ module Geoptima
120
200
  val
121
201
  end
122
202
  end
123
- def map(values)
203
+ def map(values,filter=nil)
124
204
  if @indices
125
205
  puts "StatSpec[#{self}]: #{options.inspect}" if($debug)
126
206
  vals = @indices.map{|i| values[i]}
@@ -133,12 +213,14 @@ module Geoptima
133
213
  val
134
214
  end
135
215
  end
136
- def prepare_indices(stats)
137
- if stats.headers.index(header)
216
+ def prepare_indices(stats_manager,headers)
217
+ if headers.index(header)
138
218
  puts "Header '#{header}' already exists, cannot create #{self}"
219
+ @index = nil
220
+ @indices = nil
139
221
  else
140
- @index = stats.add_header(header)
141
- @indices = @fields.map{|h| stats.headers.index(h) || stats.headers.index(h.downcase) }
222
+ @index = stats_manager.add_header(header)
223
+ @indices = @fields.map{|h| headers.index(h) || headers.index(h.downcase) }
142
224
  puts "#{self}: Header[#{@index}], Indices[#{@indices.join(',')}]" if($debug)
143
225
  if @indices.compact.length < @fields.length
144
226
  puts "Unable to find some headers for #{self}, ignoring stats"
@@ -152,35 +234,60 @@ module Geoptima
152
234
  end
153
235
  class ChartSpec
154
236
  attr_reader :chart_type, :header, :options
155
- def initialize(chart_type,header,options={})
237
+ def initialize(header,options={})
156
238
  @header = header
157
- @chart_type = chart_type
239
+ @chart_type = options[:chart_type] || :histogram
158
240
  @options = options
159
241
  end
160
- def process(stats)
161
- puts "Charting #{header} using stats.headers: #{stats.headers}"
162
- index = stats.headers.index(header)
163
- index ||= stats.headers.index(header.downcase)
164
- unless index
242
+ def process(stats_manager)
243
+ puts "Charting #{header} using headers: #{stats_manager.headers.inspect}"
244
+ stat_spec = $specs.stat_specs.find{|o| o.header == header}
245
+ stats = stats_manager.get_stats(header)
246
+ grouped_stats = {}
247
+ if stat_spec
248
+ stat_spec.groups.each do |name,header|
249
+ gs = stats_manager.get_stats(header)
250
+ grouped_stats[name] = gs
251
+ stats ||= gs
252
+ end
253
+ end
254
+ unless stats
165
255
  puts "Cannot find statistics for '#{header}' - ignoring chart"
166
256
  return
167
257
  end
168
- puts "Charting #{header} at index #{index} and options #{options.inspect}"
169
- puts "Charting #{header} with diversity #{stats.diversity(index)}"
170
- hist = stats.stats[index]
171
- title = options[:title]
172
- if options[:top]
173
- keys = hist.keys.sort{|a,b| hist[b] <=> hist[a]}[0..(options[:top].to_i)]
174
- title ||= "#{header} Top #{options[:top]}"
258
+ puts "Charting #{header} with options #{options.inspect} and stats: #{stats}"
259
+ puts "Charting #{header} with diversity #{stats.diversity}"
260
+ if grouped_stats.length > 0
261
+ title = options[:title]
262
+ title ||= "#{header} Distribution"
263
+ options.merge!( :title => title, :width => 1024 )
264
+ value_map = {}
265
+ groups = grouped_stats.keys.sort
266
+ groups.each_with_index do |name,index|
267
+ gs = grouped_stats[name]
268
+ hist = gs.stats
269
+ hist.keys.each do |k|
270
+ value_map[k] ||= [].fill(0,0...groups.length)
271
+ value_map[k][index] = hist[k]
272
+ end
273
+ end
274
+ legends = value_map.keys.sort
275
+ g = Geoptima::Chart.draw_grouped_chart legends, groups, value_map, options
175
276
  else
176
- keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
277
+ hist = stats.stats
278
+ title = options[:title]
279
+ if options[:top]
280
+ keys = hist.keys.sort{|a,b| hist[b] <=> hist[a]}[0..(options[:top].to_i)]
281
+ title ||= "#{header} Top #{options[:top]}"
282
+ else
283
+ keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
284
+ end
285
+ values = keys.map{|k| hist[k]}
286
+ title ||= "#{header} Distribution"
287
+ options.merge!( :title => title, :width => 1024 )
288
+ g = Geoptima::Chart.send "draw_#{chart_type}_chart", stats_manager.name, keys, values, options
177
289
  end
178
- values = keys.map{|k| hist[k]}
179
- title ||= "#{header} Distribution"
180
- options.merge!( :title => title, :width => 1024 )
181
- legend = $merge_all ? "ALL" : stats.name
182
- g = Geoptima::Chart.send "draw_#{chart_type}_chart", stats.name, keys, values, options
183
- g.write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
290
+ g.write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_#{chart_type}_distribution.png")
184
291
  end
185
292
  def to_s
186
293
  "#{chart_type.upcase}-#{header}"
@@ -194,126 +301,138 @@ module Geoptima
194
301
  instance_eval(File.open(specfile).read)
195
302
  end
196
303
  def category_chart(header,options={})
197
- chart(:category, header, options)
304
+ chart(header, options.merge(:chart_type => :category))
198
305
  end
199
306
  def histogram_chart(header,options={})
200
- chart(:histogram, header, options)
307
+ chart(header, options.merge(:chart_type => :histgram))
201
308
  end
202
309
  def line_chart(header,options={})
203
- chart(:line, header, options)
310
+ chart(header, options.merge(:chart_type => :line))
204
311
  end
205
- def chart(chart_type,header,options={})
206
- @chart_specs << ChartSpec.new(chart_type,header,options)
312
+ def chart(header,options={})
313
+ @chart_specs << ChartSpec.new(header,options)
207
314
  end
208
315
  def stats(header,*fields,&block)
209
316
  @stat_specs << StatSpec.new(header,*fields,&block)
210
317
  end
211
- def add_stats(stats)
318
+ def add_stats(stats_manager,headers)
212
319
  stat_specs.each do |stat_spec|
213
- stat_spec.prepare_indices(stats)
320
+ stat_spec.prepare_indices(stats_manager,headers)
214
321
  end
215
322
  end
216
- def add_fields(stats,fields)
217
- $specs.stat_specs.each do |stat_spec|
218
- stats.add(
219
- stat_spec.map(fields),
220
- stat_spec.index
221
- )
323
+ def add_fields(stats_manager,fields)
324
+ stat_specs.each do |stat_spec|
325
+ stat_spec.add(stats_manager,fields)
222
326
  end
223
327
  end
224
328
  def to_s
225
- "Charts[#{chart_specs.length}]: #{@chart_specs.join(', ')}"
226
- end
227
- end
228
- end
229
-
230
- $specfile && $specs = Geoptima::StatsSpecs.new($specfile)
231
-
232
- $files.each do |file|
233
- lines = 0
234
- filename = File.basename(file)
235
- (names = filename.split(/[_\.]/)).pop
236
- name = $merged_name || names.join('_')
237
- puts "About to read file #{file}"
238
- File.open(file).each do |line|
239
- lines += 1
240
- fields=line.chomp.split(/\t/)
241
- if $stats[file]
242
- puts "Processing line: #{line}" if($debug)
243
- fields.each_with_index do |field,index|
244
- $stats[file].add(field,index)
245
- end
246
- $specs && $specs.add_fields($stats[file],fields)
247
- elsif($merge_all && $stats.length>0)
248
- file = $stats.values[0].file
249
- else
250
- $stats[file] = Stats.new(filename,name,fields)
251
- $specs && $specs.add_stats($stats[file])
329
+ "Stats[#{@stat_specs.join(', ')}] AND Charts[#{@chart_specs.join(', ')}]"
252
330
  end
253
331
  end
254
332
  end
255
333
 
256
- $stats.each do |file,stats|
257
- if $specs
258
- $specs.chart_specs.each do |chart_spec|
259
- chart_spec.process(stats)
260
- end
261
- else
262
- stats.headers.each_with_index do |header,index|
263
- puts "Charting #{header} with diversity #{stats.diversity(index)}"
334
+ def create_all(name,stats_manager)
335
+ stats_manager.headers.each do |header|
336
+ stats = stats_manager.stats[header]
337
+ puts "Charting #{header} with diversity #{stats.diversity}"
264
338
  case header
265
339
  when 'signal.strength'
266
340
  Geoptima::Chart.draw_line_chart(
267
- stats.name,
268
- stats.data[0],
269
- stats.data[index].map{|f| v=f.to_i; (v>-130 && v<0) ? v : nil},
341
+ stats_manager.name,
342
+ stats_manager.time_stats.data,
343
+ stats.data.map{|f| v=f.to_i; (v>-130 && v<0) ? v : nil},
270
344
  :title => 'Signal Strength',
271
345
  :maximum_value => -30,
272
346
  :minimum_value => -130,
273
347
  :width => 1024
274
- ).write("#{$export_dir}/Chart_#{stats.name}_#{header}.png")
348
+ ).write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}.png")
275
349
 
276
- hist = stats.stats[index]
350
+ hist = stats.stats
277
351
  keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
278
352
  values = keys.map{|k| hist[k]}
279
353
  Geoptima::Chart.draw_histogram_chart(
280
- stats.name, keys, values,
354
+ stats_manager.name, keys, values,
281
355
  :title => 'Signal Strength Distribution',
282
356
  :width => 1024
283
- ).write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
357
+ ).write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_distribution.png")
284
358
 
285
359
  when 'Event'
286
- hist = stats.stats[index]
360
+ hist = stats.stats
287
361
  keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
288
362
  values = keys.map{|k| hist[k]}
289
363
  Geoptima::Chart.draw_category_chart(
290
- stats.name, keys, values,
364
+ stats_manager.name, keys, values,
291
365
  :title => "#{header} Distribution",
292
366
  :width => 1024
293
- ).write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
367
+ ).write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_distribution.png")
294
368
 
295
369
  else
296
- if stats.diverse?(index)
370
+ if stats.diverse?
297
371
  puts "Ignoring high diversity field #{header}"
298
372
  else
299
- puts "Charting field: #{header} with length #{stats.length(index)} and diversity #{stats.diversity(index)}"
300
- hist = stats.stats[index]
373
+ puts "Charting field: #{header} with length #{stats.length} and diversity #{stats.diversity}"
374
+ hist = stats.stats
301
375
  keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
302
376
  values = keys.map{|k| hist[k]}
303
- args = [stats.name, keys, values, {
377
+ args = [stats_manager.name, keys, values, {
304
378
  :title => "#{header} Distribution",
305
379
  :width => 1024}]
306
- g = (stats.length(index) > 50) ?
380
+ g = (stats.length > 50) ?
307
381
  Geoptima::Chart.draw_line_chart(*args) :
308
- (stats.length(index) > 10 || stats.numerical?(index)) ?
382
+ (stats.length > 10 || stats.numerical?) ?
309
383
  Geoptima::Chart.draw_histogram_chart(*args) :
310
- (stats.length(index) > 1) ?
384
+ (stats.length > 1) ?
311
385
  Geoptima::Chart.draw_category_chart(*args) :
312
386
  nil
313
- g && g.write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
387
+ g && g.write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_distribution.png")
314
388
  end
315
389
  end
316
390
  end
391
+ end
392
+
393
+ #
394
+ # Now run the actual program, reading the specifation file and then the CSV files
395
+ #
396
+
397
+ $stats_managers = {}
398
+
399
+ $specfile && $specs = Geoptima::StatsSpecs.new($specfile)
400
+
401
+ $files.each do |file|
402
+ lines = 0
403
+ headers = nil
404
+ filename = File.basename(file)
405
+ (names = filename.split(/[_\.]/)).pop
406
+ name = $merge_all ? ($merged_name || 'All') : names.join('_')
407
+ $stats_managers[name] ||= StatsManager.new(name)
408
+ puts "About to read file #{file}"
409
+ File.open(file).each do |line|
410
+ lines += 1
411
+ fields=line.chomp.split(/\t/)
412
+ if headers
413
+ puts "Processing line: #{line}" if($debug)
414
+ $stats_managers[name].add_all(fields,headers)
415
+ else
416
+ headers = fields
417
+ if headers.length<2
418
+ puts "Too few headers, rejecting #{file}"
419
+ break
420
+ end
421
+ $stats_managers[name].set_headers(headers)
422
+ end
423
+ end
424
+ end
425
+
426
+ # Finally output all charts specified
427
+
428
+ $stats_managers.each do |name,stats_manager|
429
+ if $specs
430
+ $specs.chart_specs.each do |chart_spec|
431
+ chart_spec.process(stats_manager)
432
+ end
433
+ end
434
+ if $create_all
435
+ create_all name, stats_manager
317
436
  end
318
437
  end
319
438
 
data/bin/show_geoptima CHANGED
@@ -7,7 +7,7 @@ $: << '../lib'
7
7
  require 'date'
8
8
  require 'geoptima'
9
9
 
10
- Geoptima::assert_version("0.0.9")
10
+ Geoptima::assert_version("0.1.0")
11
11
 
12
12
  $debug=false
13
13
 
@@ -8,8 +8,10 @@ require 'geoptima/chart'
8
8
  require 'geoptima/version'
9
9
  require 'geoptima/options'
10
10
  require 'fileutils'
11
+ require 'geoptima/daterange'
11
12
 
12
- Geoptima::assert_version("0.0.9")
13
+ Geoptima::assert_version("0.1.0")
14
+ Geoptima::Chart.available? || puts("No charting libraries available") || exit(-1)
13
15
 
14
16
  $export_dir = '.'
15
17
  $diversity = 40.0
@@ -17,14 +19,22 @@ $diversity = 40.0
17
19
  $files = Geoptima::Options.process_args do |option|
18
20
  option.m {$merge_all = true}
19
21
  option.a {$create_all = true}
22
+ option.t {$time_split = true}
20
23
  option.D {$export_dir = ARGV.shift}
21
24
  option.N {$merged_name = ARGV.shift}
22
25
  option.S {$specfile = ARGV.shift}
23
26
  option.P {$diversity = ARGV.shift.to_f}
27
+ option.T do
28
+ $time_range = Geoptima::DateRange.new(*(ARGV.shift.split(/[\,]+/).map do |t|
29
+ DateTime.parse t
30
+ end))
31
+ end
24
32
  end
25
33
 
26
34
  FileUtils.mkdir_p $export_dir
27
35
 
36
+ $create_all = true unless($specfile)
37
+ $merge_all = true if($time_split)
28
38
  $help = true unless($files.length>0)
29
39
  if $help
30
40
  puts <<EOHELP
@@ -33,44 +43,46 @@ Usage: csv_chart <-dham> <-S specfile> <-N name> <-D dir> files...
33
43
  -h print this help #{cw $help}
34
44
  -a automatically create charts for all properties #{cw $create_all}
35
45
  -m merge all files into single stats #{cw $merge_all}
46
+ -t merge and split by time (days) #{cw $time_split}
36
47
  -N use specified name for merged dataset: #{$merged_name}
37
48
  -D export charts to specified directory: #{$export_dir}
38
49
  -S use chart specification in specified file: #{$specfile}
39
50
  -P diversity threshold in percentage for automatic reports: #{$diversity}
51
+ -T set time-range filter: #{$time_range}
40
52
  Files to import: #{$files.join(', ')}
41
53
  EOHELP
42
54
  exit
43
55
  end
44
56
 
45
57
  class Stats
46
- attr_reader :file, :name, :headers, :stats, :data
47
- def initialize(file,name,fields)
48
- @file = file
58
+ attr_reader :name, :stats, :data, :numerical
59
+ def initialize(name)
49
60
  @name = name
50
- @headers = fields
51
- @stats = fields.map{|h| {}}
52
- @data = fields.map{|h| []}
53
- @numerical = fields.map{|h| true}
54
- end
55
- def add_header(h)
56
- @headers << h
57
- @stats << {}
58
- @data << []
59
- @numerical << true
60
- @headers.length - 1
61
+ @stats = {}
62
+ @data = []
63
+ @numerical = true
61
64
  end
62
- def add(field,index)
63
- puts "\tAdding field '#{field}' at index #{index}" if($debug)
65
+ def add(field)
66
+ puts "\tAdding field '#{field}' for property #{name}" if($debug)
64
67
  if field && field =~ /\w/
65
- @numerical[index] &&= is_number?(field)
66
- puts "\tField[#{index}]: #{field}" if($debug)
67
- stats = @stats[index]
68
+ @numerical &&= is_number?(field)
68
69
  stats[field] ||= 0
69
70
  stats[field] += 1
70
- puts "\tField[#{index}]: #{field} => #{stats[field]}" if($debug)
71
- @data[index] << field
71
+ @data << field
72
72
  end
73
73
  end
74
+ def length
75
+ @stats.length
76
+ end
77
+ def numerical?
78
+ @numerical
79
+ end
80
+ def diversity
81
+ 100.0 * @stats.length.to_f / @data.length.to_f
82
+ end
83
+ def diverse?
84
+ @stats.length>500 || diversity > $diversity
85
+ end
74
86
  def is_number?(field)
75
87
  is_integer?(field) || is_float?(field)
76
88
  end
@@ -80,36 +92,104 @@ class Stats
80
92
  def is_float?(field)
81
93
  field.to_f.to_s == field
82
94
  end
83
- def length(index)
84
- @stats[index].length
95
+ def to_s
96
+ "#{name}[#{length}]"
97
+ end
98
+ end
99
+
100
+ class StatsManager
101
+ attr_reader :name, :headers, :stats
102
+ def initialize(name)
103
+ @name = name
104
+ @headers = []
105
+ @stats = {}
106
+ end
107
+ def time_index
108
+ @time_index ||= @headers.index('Time') || @headers.index('Timestamp')
109
+ end
110
+ def time_stats
111
+ @time_stats ||= get_stats('Time') || get_stats('Timestamp')
112
+ end
113
+ def set_headers(headers)
114
+ @headers = []
115
+ headers.each {|h| add_header(h)}
116
+ $specs && $specs.add_stats(self,headers)
117
+ end
118
+ def add_header(h)
119
+ if @headers.index(h)
120
+ puts "Stats header already exists: #{h}"
121
+ else
122
+ @headers << h
123
+ @stats[h] ||= Stats.new(h)
124
+ end
125
+ @headers.index(h)
126
+ end
127
+ def get_stats(header)
128
+ stats[header] || stats[header.downcase]
129
+ end
130
+ def add_all(fields,headers)
131
+ fields.each_with_index do |field,index|
132
+ add(field,headers[index])
133
+ end
134
+ $specs && $specs.add_fields(self,fields)
85
135
  end
86
- def diversity(index)
87
- 100.0 * @stats[index].length.to_f / @data[index].length.to_f
136
+ def add(field,header)
137
+ puts "\tAdding field '#{field}' for property #{header}" if($debug)
138
+ add_header(header) unless(@stats[header])
139
+ @stats[header].add(field)
88
140
  end
89
- def diverse?(index)
90
- @stats[index].length>500 || diversity(index) > $diversity
141
+ def length
142
+ @stats.length
91
143
  end
92
- def numerical?(index)
93
- @numerical[index]
144
+ def to_s
145
+ "Stats[#{length}]: #{@stats.inspect}"
94
146
  end
95
147
  end
96
148
 
97
- $stats = {}
98
-
99
149
  module Geoptima
100
150
  class StatSpec
101
- attr_reader :header, :index, :indices, :fields, :options, :proc
151
+ attr_reader :header, :index, :indices, :fields, :options, :proc, :groups
102
152
  def initialize(header,*fields,&block)
103
153
  @header = header
104
154
  @fields = fields
105
155
  @proc = block
156
+ @groups = {}
106
157
  if @fields[-1].is_a?(Hash)
107
158
  @options = @fields.pop
108
159
  else
109
160
  @options = {}
110
161
  end
162
+ if @options[:group]
163
+ case @options[:group].to_s.intern
164
+ when :months
165
+ group_by {|t| t.strftime("%Y-%m")}
166
+ when :days
167
+ group_by {|t| t.strftime("%Y-%m-%d")}
168
+ else
169
+ group_by {|t| t.strftime("%Y-%m-%d %H")}
170
+ end
171
+ end
111
172
  puts "Created StatSpec: #{self}"
112
173
  end
174
+ def group_by(&block)
175
+ @group = block
176
+ end
177
+ def add(stats_manager,fields)
178
+ if @group
179
+ begin
180
+ time = DateTime.parse(fields[stats_manager.time_index])
181
+ if $time_range.nil? || $time_range.include?(time)
182
+ key = @group.call(time)
183
+ ghead = "#{header} #{key}"
184
+ @groups[key] = ghead
185
+ stats_manager.add(map(fields),ghead)
186
+ end
187
+ rescue ArgumentError
188
+ puts "Error: Unable to process time field[#{time}]: #{$!}"
189
+ end
190
+ end
191
+ stats_manager.add(map(fields),header)
192
+ end
113
193
  def mk_range(val)
114
194
  if val =~ /\w/
115
195
  div = options[:div].to_i
@@ -120,7 +200,7 @@ module Geoptima
120
200
  val
121
201
  end
122
202
  end
123
- def map(values)
203
+ def map(values,filter=nil)
124
204
  if @indices
125
205
  puts "StatSpec[#{self}]: #{options.inspect}" if($debug)
126
206
  vals = @indices.map{|i| values[i]}
@@ -133,12 +213,14 @@ module Geoptima
133
213
  val
134
214
  end
135
215
  end
136
- def prepare_indices(stats)
137
- if stats.headers.index(header)
216
+ def prepare_indices(stats_manager,headers)
217
+ if headers.index(header)
138
218
  puts "Header '#{header}' already exists, cannot create #{self}"
219
+ @index = nil
220
+ @indices = nil
139
221
  else
140
- @index = stats.add_header(header)
141
- @indices = @fields.map{|h| stats.headers.index(h) || stats.headers.index(h.downcase) }
222
+ @index = stats_manager.add_header(header)
223
+ @indices = @fields.map{|h| headers.index(h) || headers.index(h.downcase) }
142
224
  puts "#{self}: Header[#{@index}], Indices[#{@indices.join(',')}]" if($debug)
143
225
  if @indices.compact.length < @fields.length
144
226
  puts "Unable to find some headers for #{self}, ignoring stats"
@@ -152,35 +234,60 @@ module Geoptima
152
234
  end
153
235
  class ChartSpec
154
236
  attr_reader :chart_type, :header, :options
155
- def initialize(chart_type,header,options={})
237
+ def initialize(header,options={})
156
238
  @header = header
157
- @chart_type = chart_type
239
+ @chart_type = options[:chart_type] || :histogram
158
240
  @options = options
159
241
  end
160
- def process(stats)
161
- puts "Charting #{header} using stats.headers: #{stats.headers}"
162
- index = stats.headers.index(header)
163
- index ||= stats.headers.index(header.downcase)
164
- unless index
242
+ def process(stats_manager)
243
+ puts "Charting #{header} using headers: #{stats_manager.headers.inspect}"
244
+ stat_spec = $specs.stat_specs.find{|o| o.header == header}
245
+ stats = stats_manager.get_stats(header)
246
+ grouped_stats = {}
247
+ if stat_spec
248
+ stat_spec.groups.each do |name,header|
249
+ gs = stats_manager.get_stats(header)
250
+ grouped_stats[name] = gs
251
+ stats ||= gs
252
+ end
253
+ end
254
+ unless stats
165
255
  puts "Cannot find statistics for '#{header}' - ignoring chart"
166
256
  return
167
257
  end
168
- puts "Charting #{header} at index #{index} and options #{options.inspect}"
169
- puts "Charting #{header} with diversity #{stats.diversity(index)}"
170
- hist = stats.stats[index]
171
- title = options[:title]
172
- if options[:top]
173
- keys = hist.keys.sort{|a,b| hist[b] <=> hist[a]}[0..(options[:top].to_i)]
174
- title ||= "#{header} Top #{options[:top]}"
258
+ puts "Charting #{header} with options #{options.inspect} and stats: #{stats}"
259
+ puts "Charting #{header} with diversity #{stats.diversity}"
260
+ if grouped_stats.length > 0
261
+ title = options[:title]
262
+ title ||= "#{header} Distribution"
263
+ options.merge!( :title => title, :width => 1024 )
264
+ value_map = {}
265
+ groups = grouped_stats.keys.sort
266
+ groups.each_with_index do |name,index|
267
+ gs = grouped_stats[name]
268
+ hist = gs.stats
269
+ hist.keys.each do |k|
270
+ value_map[k] ||= [].fill(0,0...groups.length)
271
+ value_map[k][index] = hist[k]
272
+ end
273
+ end
274
+ legends = value_map.keys.sort
275
+ g = Geoptima::Chart.draw_grouped_chart legends, groups, value_map, options
175
276
  else
176
- keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
277
+ hist = stats.stats
278
+ title = options[:title]
279
+ if options[:top]
280
+ keys = hist.keys.sort{|a,b| hist[b] <=> hist[a]}[0..(options[:top].to_i)]
281
+ title ||= "#{header} Top #{options[:top]}"
282
+ else
283
+ keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
284
+ end
285
+ values = keys.map{|k| hist[k]}
286
+ title ||= "#{header} Distribution"
287
+ options.merge!( :title => title, :width => 1024 )
288
+ g = Geoptima::Chart.send "draw_#{chart_type}_chart", stats_manager.name, keys, values, options
177
289
  end
178
- values = keys.map{|k| hist[k]}
179
- title ||= "#{header} Distribution"
180
- options.merge!( :title => title, :width => 1024 )
181
- legend = $merge_all ? "ALL" : stats.name
182
- g = Geoptima::Chart.send "draw_#{chart_type}_chart", stats.name, keys, values, options
183
- g.write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
290
+ g.write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_#{chart_type}_distribution.png")
184
291
  end
185
292
  def to_s
186
293
  "#{chart_type.upcase}-#{header}"
@@ -194,126 +301,138 @@ module Geoptima
194
301
  instance_eval(File.open(specfile).read)
195
302
  end
196
303
  def category_chart(header,options={})
197
- chart(:category, header, options)
304
+ chart(header, options.merge(:chart_type => :category))
198
305
  end
199
306
  def histogram_chart(header,options={})
200
- chart(:histogram, header, options)
307
+ chart(header, options.merge(:chart_type => :histgram))
201
308
  end
202
309
  def line_chart(header,options={})
203
- chart(:line, header, options)
310
+ chart(header, options.merge(:chart_type => :line))
204
311
  end
205
- def chart(chart_type,header,options={})
206
- @chart_specs << ChartSpec.new(chart_type,header,options)
312
+ def chart(header,options={})
313
+ @chart_specs << ChartSpec.new(header,options)
207
314
  end
208
315
  def stats(header,*fields,&block)
209
316
  @stat_specs << StatSpec.new(header,*fields,&block)
210
317
  end
211
- def add_stats(stats)
318
+ def add_stats(stats_manager,headers)
212
319
  stat_specs.each do |stat_spec|
213
- stat_spec.prepare_indices(stats)
320
+ stat_spec.prepare_indices(stats_manager,headers)
214
321
  end
215
322
  end
216
- def add_fields(stats,fields)
217
- $specs.stat_specs.each do |stat_spec|
218
- stats.add(
219
- stat_spec.map(fields),
220
- stat_spec.index
221
- )
323
+ def add_fields(stats_manager,fields)
324
+ stat_specs.each do |stat_spec|
325
+ stat_spec.add(stats_manager,fields)
222
326
  end
223
327
  end
224
328
  def to_s
225
- "Charts[#{chart_specs.length}]: #{@chart_specs.join(', ')}"
226
- end
227
- end
228
- end
229
-
230
- $specfile && $specs = Geoptima::StatsSpecs.new($specfile)
231
-
232
- $files.each do |file|
233
- lines = 0
234
- filename = File.basename(file)
235
- (names = filename.split(/[_\.]/)).pop
236
- name = $merged_name || names.join('_')
237
- puts "About to read file #{file}"
238
- File.open(file).each do |line|
239
- lines += 1
240
- fields=line.chomp.split(/\t/)
241
- if $stats[file]
242
- puts "Processing line: #{line}" if($debug)
243
- fields.each_with_index do |field,index|
244
- $stats[file].add(field,index)
245
- end
246
- $specs && $specs.add_fields($stats[file],fields)
247
- elsif($merge_all && $stats.length>0)
248
- file = $stats.values[0].file
249
- else
250
- $stats[file] = Stats.new(filename,name,fields)
251
- $specs && $specs.add_stats($stats[file])
329
+ "Stats[#{@stat_specs.join(', ')}] AND Charts[#{@chart_specs.join(', ')}]"
252
330
  end
253
331
  end
254
332
  end
255
333
 
256
- $stats.each do |file,stats|
257
- if $specs
258
- $specs.chart_specs.each do |chart_spec|
259
- chart_spec.process(stats)
260
- end
261
- else
262
- stats.headers.each_with_index do |header,index|
263
- puts "Charting #{header} with diversity #{stats.diversity(index)}"
334
+ def create_all(name,stats_manager)
335
+ stats_manager.headers.each do |header|
336
+ stats = stats_manager.stats[header]
337
+ puts "Charting #{header} with diversity #{stats.diversity}"
264
338
  case header
265
339
  when 'signal.strength'
266
340
  Geoptima::Chart.draw_line_chart(
267
- stats.name,
268
- stats.data[0],
269
- stats.data[index].map{|f| v=f.to_i; (v>-130 && v<0) ? v : nil},
341
+ stats_manager.name,
342
+ stats_manager.time_stats.data,
343
+ stats.data.map{|f| v=f.to_i; (v>-130 && v<0) ? v : nil},
270
344
  :title => 'Signal Strength',
271
345
  :maximum_value => -30,
272
346
  :minimum_value => -130,
273
347
  :width => 1024
274
- ).write("#{$export_dir}/Chart_#{stats.name}_#{header}.png")
348
+ ).write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}.png")
275
349
 
276
- hist = stats.stats[index]
350
+ hist = stats.stats
277
351
  keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
278
352
  values = keys.map{|k| hist[k]}
279
353
  Geoptima::Chart.draw_histogram_chart(
280
- stats.name, keys, values,
354
+ stats_manager.name, keys, values,
281
355
  :title => 'Signal Strength Distribution',
282
356
  :width => 1024
283
- ).write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
357
+ ).write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_distribution.png")
284
358
 
285
359
  when 'Event'
286
- hist = stats.stats[index]
360
+ hist = stats.stats
287
361
  keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
288
362
  values = keys.map{|k| hist[k]}
289
363
  Geoptima::Chart.draw_category_chart(
290
- stats.name, keys, values,
364
+ stats_manager.name, keys, values,
291
365
  :title => "#{header} Distribution",
292
366
  :width => 1024
293
- ).write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
367
+ ).write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_distribution.png")
294
368
 
295
369
  else
296
- if stats.diverse?(index)
370
+ if stats.diverse?
297
371
  puts "Ignoring high diversity field #{header}"
298
372
  else
299
- puts "Charting field: #{header} with length #{stats.length(index)} and diversity #{stats.diversity(index)}"
300
- hist = stats.stats[index]
373
+ puts "Charting field: #{header} with length #{stats.length} and diversity #{stats.diversity}"
374
+ hist = stats.stats
301
375
  keys = hist.keys.sort{|a,b| a.to_i <=> b.to_i}
302
376
  values = keys.map{|k| hist[k]}
303
- args = [stats.name, keys, values, {
377
+ args = [stats_manager.name, keys, values, {
304
378
  :title => "#{header} Distribution",
305
379
  :width => 1024}]
306
- g = (stats.length(index) > 50) ?
380
+ g = (stats.length > 50) ?
307
381
  Geoptima::Chart.draw_line_chart(*args) :
308
- (stats.length(index) > 10 || stats.numerical?(index)) ?
382
+ (stats.length > 10 || stats.numerical?) ?
309
383
  Geoptima::Chart.draw_histogram_chart(*args) :
310
- (stats.length(index) > 1) ?
384
+ (stats.length > 1) ?
311
385
  Geoptima::Chart.draw_category_chart(*args) :
312
386
  nil
313
- g && g.write("#{$export_dir}/Chart_#{stats.name}_#{header}_distribution.png")
387
+ g && g.write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_distribution.png")
314
388
  end
315
389
  end
316
390
  end
391
+ end
392
+
393
+ #
394
+ # Now run the actual program, reading the specifation file and then the CSV files
395
+ #
396
+
397
+ $stats_managers = {}
398
+
399
+ $specfile && $specs = Geoptima::StatsSpecs.new($specfile)
400
+
401
+ $files.each do |file|
402
+ lines = 0
403
+ headers = nil
404
+ filename = File.basename(file)
405
+ (names = filename.split(/[_\.]/)).pop
406
+ name = $merge_all ? ($merged_name || 'All') : names.join('_')
407
+ $stats_managers[name] ||= StatsManager.new(name)
408
+ puts "About to read file #{file}"
409
+ File.open(file).each do |line|
410
+ lines += 1
411
+ fields=line.chomp.split(/\t/)
412
+ if headers
413
+ puts "Processing line: #{line}" if($debug)
414
+ $stats_managers[name].add_all(fields,headers)
415
+ else
416
+ headers = fields
417
+ if headers.length<2
418
+ puts "Too few headers, rejecting #{file}"
419
+ break
420
+ end
421
+ $stats_managers[name].set_headers(headers)
422
+ end
423
+ end
424
+ end
425
+
426
+ # Finally output all charts specified
427
+
428
+ $stats_managers.each do |name,stats_manager|
429
+ if $specs
430
+ $specs.chart_specs.each do |chart_spec|
431
+ chart_spec.process(stats_manager)
432
+ end
433
+ end
434
+ if $create_all
435
+ create_all name, stats_manager
317
436
  end
318
437
  end
319
438
 
@@ -7,7 +7,7 @@ $: << '../lib'
7
7
  require 'date'
8
8
  require 'geoptima'
9
9
 
10
- Geoptima::assert_version("0.0.9")
10
+ Geoptima::assert_version("0.1.0")
11
11
 
12
12
  $debug=false
13
13
 
@@ -20,6 +20,12 @@ end
20
20
 
21
21
  module Geoptima
22
22
  class Chart
23
+ def self.libs
24
+ $chart_libs
25
+ end
26
+ def self.available?
27
+ $chart_libs.length>0
28
+ end
23
29
  DEFAULT_OPTIONS = {:show_points => true, :show_lines => true, :title => nil, :width => 800, :margins => 20, :font_size => 14}
24
30
  attr_reader :chart_type
25
31
  attr_accessor :chart, :data, :options
@@ -55,6 +61,18 @@ module Geoptima
55
61
  options[:filename] && g.write(options[:filename])
56
62
  g
57
63
  end
64
+ def self.draw_grouped_chart(legends,keys,values,options={})
65
+ puts "Creating a chart with legends #{legends.inspect} for #{keys.length} keys"
66
+ chart_type = (options[:chart_type]==:line) ? :line : :bar
67
+ g = make_chart(chart_type, options)
68
+ legends.each do |legend|
69
+ g.data(legend, values[legend])
70
+ end
71
+ g.minimum_value = 0
72
+ g.labels = keys.inject({}){|a,v| a[a.length] = v;a}
73
+ options[:filename] && g.write(options[:filename])
74
+ g
75
+ end
58
76
  def self.draw_category_chart(legend,keys,values,options={})
59
77
  puts "Creating category chart with keys: #{keys.join(',')}"
60
78
  puts "Creating category chart with values: #{values.join(',')}"
data/lib/geoptima/data.rb CHANGED
@@ -2,21 +2,7 @@
2
2
 
3
3
  require 'rubygems'
4
4
  require 'multi_json'
5
- require 'date'
6
- if $use_dateperformance
7
- begin
8
- require 'date/performance'
9
- require 'date/memoize'
10
- class DateTime
11
- def >(other) ; self - other > 0 ; end
12
- def <(other) ; self - other < 0 ; end
13
- def >=(other); self - other >= 0; end
14
- def <=(other); self - other <= 0; end
15
- end
16
- rescue LoadError
17
- puts "No date-performance gem installed, some features will run slower"
18
- end
19
- end
5
+ require 'geoptima/daterange'
20
6
 
21
7
  #
22
8
  # The Geoptima Module provides support for the Geoptima Client JSON file format
@@ -43,26 +29,6 @@ module Geoptima
43
29
  end
44
30
  end
45
31
 
46
- class DateRange
47
- attr_reader :min, :max, :range
48
- def initialize(min,max)
49
- @min = min
50
- @max = max
51
- @range = Range.new(min,max)
52
- end
53
- if ENV['RUBY_VERSION'] =~ /1\.8/
54
- puts "Defining Range.include? to wrap for 1.8"
55
- def include?(time)
56
- @range.include?(time)
57
- end
58
- else
59
- puts "Defining Range.include? to perform inequality tests for 1.9"
60
- def include?(time)
61
- (time >= min) && (time <= @max)
62
- end
63
- end
64
- end
65
-
66
32
  # The Geoptima::Event class represents and individual record or event
67
33
  class Event
68
34
  KNOWN_HEADERS={
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'date'
4
+ if $use_dateperformance
5
+ begin
6
+ require 'date/performance'
7
+ require 'date/memoize'
8
+ class DateTime
9
+ def >(other) ; self - other > 0 ; end
10
+ def <(other) ; self - other < 0 ; end
11
+ def >=(other); self - other >= 0; end
12
+ def <=(other); self - other <= 0; end
13
+ end
14
+ rescue LoadError
15
+ puts "No date-performance gem installed, some features will run slower"
16
+ end
17
+ end
18
+
19
+ module Geoptima
20
+ class DateRange
21
+ attr_reader :min, :max, :range
22
+ def initialize(min,max)
23
+ @min = min
24
+ @max = max
25
+ @range = Range.new(min,max)
26
+ end
27
+ if ENV['RUBY_VERSION'] =~ /1\.8/
28
+ puts "Defining Range.include? to wrap for 1.8"
29
+ def include?(time)
30
+ @range.include?(time)
31
+ end
32
+ else
33
+ puts "Defining Range.include? to perform inequality tests for 1.9"
34
+ def include?(time)
35
+ (time >= min) && (time <= @max)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -1,6 +1,6 @@
1
1
  module Geoptima
2
2
 
3
- VERSION = "0.0.9"
3
+ VERSION = "0.1.0"
4
4
 
5
5
  def self.version_as_int(ver)
6
6
  base = 1
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geoptima
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 9
10
- version: 0.0.9
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Craig Taverner
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-03-21 00:00:00 Z
18
+ date: 2012-03-22 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: multi_json
@@ -78,6 +78,7 @@ files:
78
78
  - lib/geoptima/data.rb
79
79
  - lib/geoptima/chart.rb
80
80
  - lib/geoptima/options.rb
81
+ - lib/geoptima/daterange.rb
81
82
  - lib/geoptima.rb
82
83
  - examples/show_geoptima_sos.rb
83
84
  - examples/show_geoptima.rb