geoptima 0.0.9 → 0.1.0

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