geoptima 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # useful if being run inside a source code checkout
4
+ $: << 'lib'
5
+ $: << '../lib'
6
+
7
+ require 'date'
8
+ require 'geoptima'
9
+
10
+ $debug=false
11
+
12
+ $event_names=[]
13
+ $files = []
14
+ $print_limit = 10000
15
+
16
+ def cw(val)
17
+ val.nil? ? '' : "(#{val})"
18
+ end
19
+
20
+ while arg=ARGV.shift:
21
+ if arg =~ /^\-(\w+)/
22
+ $1.split(//).each do |aa|
23
+ case aa
24
+ when 'h'
25
+ $help=true
26
+ when 'd'
27
+ $debug=true
28
+ when 'p'
29
+ $print=true
30
+ when 'v'
31
+ $verbose=true
32
+ when 'x'
33
+ $export=true
34
+ when 's'
35
+ $seperate=true
36
+ when 'E'
37
+ $event_names += ARGV.shift.split(/[\,\;\:\.]+/)
38
+ when 'T'
39
+ $time_range = Range.new(*(ARGV.shift.split(/[\,]+/).map do |t|
40
+ DateTime.parse t
41
+ end))
42
+ when 'L'
43
+ $print_limit = ARGV.shift.to_i
44
+ $print_limit = 10 if($print_limit<1)
45
+ else
46
+ puts "Unrecognized option: -#{aa}"
47
+ end
48
+ end
49
+ else
50
+ if File.exist? arg
51
+ $files << arg
52
+ else
53
+ puts "No such file: #{arg}"
54
+ end
55
+ end
56
+ end
57
+
58
+ $help = true if($files.length < 1)
59
+ if $help
60
+ puts <<EOHELP
61
+ Usage: ./showGeoptimaEvents.rb <-dvxEh> <-L limit> <-E types> <-T min,max> file <files>
62
+ -d debug mode (output more context during processing) #{cw $debug}
63
+ -p print mode (print out final results to console) #{cw $print}
64
+ -v verbose mode (output extra information to console) #{cw $verbose}
65
+ -x export IMEI specific CSV files for further processing #{cw $export}
66
+ -s seperate the export files by event type #{cw $seperate}
67
+ -h show this help
68
+ -L limit verbose output to specific number of lines #{cw $print_limit}
69
+ -E comma-seperated list of event types to show and export (default: all; current: #{$event_names.join(',')})
70
+ -T time range to limit results to (default: all; current: #{$time_range})
71
+ EOHELP
72
+ exit 0
73
+ end
74
+
75
+ $verbose = $verbose || $debug
76
+ $datasets = Geoptima::Dataset.make_datasets($files, :locate => true, :time_range => $time_range)
77
+
78
+ class Export
79
+ attr_reader :files, :names, :headers
80
+ def initialize(imei,names,dataset)
81
+ @names = names
82
+ if $export
83
+ if $seperate
84
+ @files = names.inject({}) do |a,name|
85
+ a[name] = File.open("#{imei}_#{name}.csv",'w')
86
+ a
87
+ end
88
+ else
89
+ @files={nil => File.open("#{imei}.csv",'w')}
90
+ end
91
+ end
92
+ @headers = names.inject({}) do |a,name|
93
+ a[name] = dataset.header([name]).reject{|h| h === 'timeoffset'}
94
+ puts "Created header for name #{name}: #{a[name].join(',')}" if($debug)
95
+ a
96
+ end
97
+ @headers[nil] = @headers.values.flatten
98
+ files && files.each do |key,file|
99
+ file.puts "Time\tEvent\tLatitude\tLongitude\t#{header(key).join("\t")}\n"
100
+ end
101
+ if $debug || $verbose
102
+ @headers.each do |name,head|
103
+ puts "Header[#{name}]: #{head.join(',')}"
104
+ end
105
+ end
106
+ end
107
+ def header(name=nil)
108
+ @headers[name]
109
+ end
110
+ def puts_to(line,name)
111
+ name = nil unless($seperate)
112
+ files[name].puts(line) if($export && files[name])
113
+ end
114
+ def puts_to_all(line)
115
+ files && files.each do |key,file|
116
+ file.puts line
117
+ end
118
+ end
119
+ def close
120
+ files && files.each do |key,file|
121
+ file.close
122
+ @files[key] = nil
123
+ end
124
+ end
125
+ end
126
+
127
+ def if_le
128
+ $count ||= 0
129
+ if $print
130
+ if $count < $print_limit
131
+ yield
132
+ elsif $count == $print_limit
133
+ puts " ... "
134
+ end
135
+ end
136
+ $count += 1
137
+ end
138
+
139
+ puts "Found #{$datasets.length} IMEIs"
140
+ $datasets.keys.sort.each do |imei|
141
+ dataset = $datasets[imei]
142
+ events = dataset.sorted
143
+ puts if($print)
144
+ puts "Found #{dataset}"
145
+ if events && ($print || $export)
146
+ names = $event_names
147
+ names = dataset.events_names if(names.length<1)
148
+ export = Export.new(imei,names,dataset)
149
+ events.each do |event|
150
+ names.each do |name|
151
+ if event.name === name
152
+ fields = export.header($seperate ? name : nil).map{|h| event[h]}
153
+ export.puts_to "#{event.time_key}\t#{event.name}\t#{event.latitude}\t#{event.longitude}\t#{fields.join("\t")}", name
154
+ if_le{puts "#{event.time_key}\t#{event.name}\t#{event.latitude}\t#{event.longitude}\t#{event.fields.inspect}"}
155
+ end
156
+ end
157
+ end
158
+ export.close
159
+ end
160
+ end
161
+
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # useful if being run inside a source code checkout
4
+ $: << 'lib'
5
+ $: << '../lib'
6
+
7
+ require 'date'
8
+ require 'geoptima'
9
+
10
+ $debug=false
11
+
12
+ $events={}
13
+ $files = []
14
+
15
+ $events={}
16
+
17
+ while arg=ARGV.shift:
18
+ if arg =~ /^\-(\w+)/
19
+ $1.split(//).each do |aa|
20
+ case aa
21
+ when 'd'
22
+ $debug=true
23
+ else
24
+ puts "Unrecognized option: -#{aa}"
25
+ end
26
+ end
27
+ else
28
+ if File.exist? arg
29
+ $files << arg
30
+ else
31
+ puts "No such file: #{arg}"
32
+ end
33
+ end
34
+ end
35
+
36
+ $datasets = Geoptima::Dataset.make_datasets($files, :locate => true)
37
+
38
+ puts "Found #{$datasets.length} IMEIs"
39
+ $datasets.keys.sort.each do |imei|
40
+ dataset = $datasets[imei]
41
+ events = dataset.sorted('mode')
42
+ puts "\nFor #{imei} found #{dataset.length} events:\n"
43
+ if events && events.length>0
44
+ puts "Time\tLat\tLon\t#{events.first.header[1..-1].join("\t")}"
45
+ events.each do |event|
46
+ puts "#{event.time}\t#{event.latitude}\t#{event.longitude}\t#{event.data[1..-1].join("\t")}"
47
+ end
48
+ end
49
+ end
50
+
data/examples/stats.rb ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $debug = false
4
+ $lines = 0
5
+
6
+ ARGF.each do |line|
7
+ $lines += 1
8
+ fields=line.chomp.split(/\t/)
9
+ if $headers
10
+ puts "Processing line: #{line}" if($debug)
11
+ fields.each_with_index do |field,index|
12
+ puts "\tField[#{index}]: #{field}" if($debug)
13
+ stats = $stats[index]
14
+ stats[field] ||= 0
15
+ stats[field] += 1
16
+ puts "\tField[#{index}]: #{field} => #{stats[field]}" if($debug)
17
+ end
18
+ else
19
+ $headers = fields
20
+ $stats = fields.map{|h| {}}
21
+ end
22
+ end
23
+
24
+ $headers.each_with_index do |header,index|
25
+ stats = $stats[index]
26
+ values = stats.keys
27
+ perc = 100.0 * values.length.to_f / $lines.to_f
28
+ puts "\nFound #{values.length} unique values for field '#{header}'"
29
+ if values.length > 500
30
+ puts "\tNot printing more values more diverse than 500"
31
+ elsif (perc > 75)
32
+ puts "\tNot printing more values more diverse than #{perc}%"
33
+ else
34
+ puts header
35
+ values.sort.each do |value|
36
+ value_text = (value.to_s.length < 1) ? '<empty>' : value
37
+ puts "\t#{value_text}\t#{stats[value]}"
38
+ end
39
+ end
40
+ end
41
+
data/geoptima.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'geoptima/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "geoptima"
8
+ s.version = Geoptima::VERSION
9
+ s.authors = "Craig Taverner"
10
+ s.email = 'craig@amanzi.com'
11
+ s.homepage = "http://github.com/craigtaverner/geoptima.rb"
12
+ s.rubyforge_project = 'geoptima'
13
+ s.summary = "Ruby access to Geoptima JSON files"
14
+ s.description = <<-EOF
15
+ Geoptima is a suite of applications for measuring and locating mobile/cellular subscriber experience on GPS enabled smartphones.
16
+ It is produced by AmanziTel AB in Helsingborg, Sweden, and supports many phone manufacturers, with free downloads from the
17
+ various app stores, markets or marketplaces. This Ruby library is only capable of reading the JSON format files priduced by these phones
18
+ and reformating them as CSV for further analysis in Excel. This is a simple and independent way of analysing the data, when
19
+ compared to the full-featured analysis applications and servers available from AmanziTel. If you want to analyse a limited amount
20
+ of data in excel, or with Ruby, then this GEM might be for you. If you want to analyse large amounts of data, from many subscribers, or over long periods of time
21
+ then rather consider the NetView and Customer IQ applications from AmanziTel at www.amanzitel.com.
22
+ EOF
23
+
24
+ s.require_path = 'lib'
25
+ s.files = Dir.glob("{bin,lib,examples}/**/*").reject{|x| x=~/(tmp|target|test-data)/} + %w(README.rdoc CHANGELOG CONTRIBUTORS Gemfile geoptima.gemspec)
26
+ s.executables = ['show_geoptima']
27
+ s.has_rdoc = true
28
+ s.extra_rdoc_files = %w( README.rdoc )
29
+ s.rdoc_options = ["--quiet", "--title", "geoptima.rb", "--opname", "index.html", "--line-numbers", "--main", "README.rdoc", "--inline-source"]
30
+ s.required_ruby_version = ">= 1.8.7"
31
+ end
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+ require 'date'
6
+
7
+ #
8
+ # The Geoptima Module provides support for the Geoptima Client JSON file format
9
+ #
10
+ module Geoptima
11
+
12
+ SPERDAY = 60*60*24
13
+ MSPERDAY = 1000*60*60*24
14
+ SHORT = 256*256
15
+
16
+ class Config
17
+ DEFAULT={:min_datetime => DateTime.parse("2000-01-01"), :max_datetime => DateTime.parse("2030-01-01")}
18
+ def self.config(options={})
19
+ @@options = (@@options || DEFAULT).merge(options)
20
+ end
21
+ def self.options
22
+ @@options ||= DEFAULT
23
+ end
24
+ def self.[](key)
25
+ options[key]
26
+ end
27
+ def self.[]=(key,value)
28
+ options[key] = value
29
+ end
30
+ end
31
+
32
+ # The Geoptima::Event class represents and individual record or event
33
+ class Event
34
+ attr_reader :header, :name, :data, :fields, :time, :latitude, :longitude
35
+ def initialize(start,name,header,data)
36
+ @name = name
37
+ @header = header
38
+ @data = data
39
+ @fields = @header.inject({}) do |a,v|
40
+ a[v] = @data[a.length]
41
+ a
42
+ end
43
+ @time = start + (@fields['timeoffset'].to_f / MSPERDAY.to_f)
44
+ @fields.reject!{|k,v| k=~/timeoffset/}
45
+ if @fields['cell_id'].to_i > SHORT
46
+ @fields['cell_id'] = @fields['cell_id'].to_i % SHORT
47
+ end
48
+ puts "Created Event: #{self}" if($debug)
49
+ end
50
+ def utc
51
+ time.new_offset(0)
52
+ end
53
+ def time_key
54
+ utc.strftime("%Y-%m-%d %H:%M:%S.%3N")
55
+ end
56
+ def [](key)
57
+ @fields[key]
58
+ end
59
+ def -(other)
60
+ (self.time - other.time) * SPERDAY
61
+ end
62
+ def closer_than(other,seconds=60)
63
+ (self - other).abs < seconds
64
+ end
65
+ def locate(gps)
66
+ @latitude = gps['latitude']
67
+ @longitude = gps['longitude']
68
+ end
69
+ def locate_if_closer_than(gps,seconds=60)
70
+ locate(gps) if(closer_than(gps,seconds))
71
+ end
72
+ def to_s
73
+ "#{name}[#{time}]: #{@fields.inspect}"
74
+ end
75
+ end
76
+
77
+ # The Geoptima::Data is an entire JSON file of events
78
+ class Data
79
+ attr_reader :path, :json, :count
80
+ def initialize(path)
81
+ @path = path
82
+ @json = JSON.parse(File.read(path))
83
+ if $debug
84
+ puts "Read Geoptima: #{geoptima}"
85
+ puts "\tSubscriber: #{subscriber.to_json}"
86
+ puts "\tIMSI: #{imsi}"
87
+ puts "\tIMEI: #{imei}"
88
+ puts "\tStart: #{start}"
89
+ end
90
+ end
91
+ def to_s
92
+ json.to_json[0..100]
93
+ end
94
+ def geoptima
95
+ @geoptima ||= json['geoptima']
96
+ end
97
+ def subscriber
98
+ @subscriber ||= geoptima['subscriber']
99
+ end
100
+ def imsi
101
+ @imsi ||= subscriber['imsi']
102
+ end
103
+ def imei
104
+ @imei ||= subscriber['imei']
105
+ end
106
+ def platform
107
+ @platform ||= subscriber['Platform'] || subscriber['platform']
108
+ end
109
+ def model
110
+ @model ||= subscriber['Model'] || subscriber['model']
111
+ end
112
+ def os
113
+ @os ||= subscriber['OS']
114
+ end
115
+ def start
116
+ @start ||= subscriber['start'] && DateTime.parse(subscriber['start'].gsub(/Asia\/Bangkok/,'GMT+7').gsub(/Mar 17 2044/,'Feb 14 2012'))
117
+ end
118
+ def valid?
119
+ start && start > Data.min_start && start < Data.max_start
120
+ end
121
+ def self.min_start
122
+ @@min_start ||= DateTime.parse("2010-01-01 00:00:00")
123
+ end
124
+ def self.max_start
125
+ @@max_start ||= DateTime.parse("2020-01-01 00:00:00")
126
+ end
127
+ def events
128
+ @events ||= make_events
129
+ end
130
+ def events_names
131
+ events.keys.sort
132
+ end
133
+ def make_hash(name)
134
+ geoptima[name].inject({}) do |a,md|
135
+ key = md.keys[0]
136
+ a[key]=md[key]
137
+ a
138
+ end
139
+ end
140
+ def make_events
141
+ @count = 0
142
+ @events_metadata = make_hash('events-metadata')
143
+ events_data = {}
144
+ geoptima['events'].each do |data|
145
+ events = data['values']
146
+ event_type = data.keys.reject{|k| k=~/values/}[0]
147
+ header = @events_metadata[event_type]
148
+ if header
149
+ events_data[event_type] = (0...data[event_type].to_i).inject([]) do |a,block|
150
+ index = header.length * block
151
+ data = events[index...(index+header.length)]
152
+ if data && data.length == header.length
153
+ @count += 1
154
+ a << Event.new(start,event_type,header,data)
155
+ else
156
+ puts "Invalid '#{event_type}' data block #{block}: #{data.inspect}"
157
+ break a
158
+ end
159
+ end
160
+ if $debug
161
+ puts "Have '#{event_type}' event data:"
162
+ puts "\t#{header.join("\t")}"
163
+ events_data[event_type].each do |d|
164
+ puts "\t#{d.data.join("\t")}"
165
+ end
166
+ end
167
+ else
168
+ puts "No header found for event type: #{event_type}"
169
+ end
170
+ end
171
+ events_data
172
+ end
173
+ end
174
+
175
+ class Dataset
176
+
177
+ attr_reader :imei, :options
178
+
179
+ def initialize(imei,options={})
180
+ @imei = imei
181
+ @data = []
182
+ @options = options
183
+ @time_range = options[:time_range] || Range.new(Config[:min_datetime],Config[:max_datetime])
184
+ end
185
+
186
+ def <<(data)
187
+ @sorted = nil
188
+ @data << data
189
+ end
190
+
191
+ def file_count
192
+ @data.length
193
+ end
194
+
195
+ def imsi
196
+ imsis[0]
197
+ end
198
+
199
+ def imsis
200
+ @imsis ||= @data.inject({}) do |a,d|
201
+ a[d.imsi] ||= 0
202
+ a[d.imsi] += d.count.to_i
203
+ a
204
+ end.to_a.sort do |a,b|
205
+ b[1]<=>a[1]
206
+ end.map do |x|
207
+ #puts "Have IMSI: #{x.join('=')}"
208
+ x[0]
209
+ end.compact.uniq
210
+ end
211
+
212
+ def platform
213
+ @platform ||= @data.map{|d| d.platform}.compact.uniq[0]
214
+ end
215
+
216
+ def model
217
+ @model ||= @data.map{|d| d.model}.compact.uniq[0]
218
+ end
219
+
220
+ def os
221
+ @os ||= @data.map{|d| d.os}.compact.uniq[0]
222
+ end
223
+
224
+ def first
225
+ merge_events unless @sorted
226
+ @sorted[nil][0]
227
+ end
228
+
229
+ def last
230
+ merge_events unless @sorted
231
+ @sorted[nil][-1]
232
+ end
233
+
234
+ def length
235
+ sorted.length
236
+ end
237
+
238
+ def sorted(event_type=nil)
239
+ merge_events unless @sorted
240
+ unless @sorted[event_type] || event_type.nil?
241
+ @sorted[event_type] = @sorted[nil].reject do |event|
242
+ event.name != event_type
243
+ end
244
+ end
245
+ @sorted[event_type]
246
+ end
247
+
248
+ def header(names=nil)
249
+ merge_events unless @sorted
250
+ (names || events_names).map do |name|
251
+ [(s=sorted(name)[0]) && s.header]
252
+ end.flatten
253
+ end
254
+
255
+ def events_names
256
+ @data.map{ |v| v.events_names }.flatten.uniq.sort
257
+ end
258
+
259
+ def merge_events
260
+ @sorted ||= {}
261
+ unless @sorted[nil]
262
+ event_hash = {}
263
+ events_names.each do |name|
264
+ @data.each do |data|
265
+ (events = data.events[name]) && events.each do |event|
266
+ if @time_range.include?(event.time)
267
+ key = "#{event.time_key} #{name}"
268
+ event_hash[key] = event
269
+ end
270
+ end
271
+ end
272
+ end
273
+ @sorted[nil] = event_hash.keys.sort.map{|k| event_hash[k]}
274
+ locate_events if(options[:locate])
275
+ end
276
+ @sorted
277
+ end
278
+
279
+ def locate_events
280
+ prev_gps = nil
281
+ sorted.each do |event|
282
+ if event.name === 'gps'
283
+ prev_gps = event
284
+ elsif prev_gps
285
+ event.locate_if_closer_than(prev_gps,60)
286
+ end
287
+ end
288
+ end
289
+
290
+ def to_s
291
+ "IMEI:#{imei}, IMSI:#{imsis.join(',')}, Platform:#{platform}, Model:#{model}, OS:#{os}, Files:#{file_count}, Events:#{sorted.length}"
292
+ end
293
+
294
+ def self.make_datasets(files, options={})
295
+ datasets = {}
296
+ files.each do |file|
297
+ geoptima=Geoptima::Data.new(file)
298
+ unless geoptima.valid?
299
+ puts "INVALID: #{geoptima.start}\t#{file}\n\n"
300
+ else
301
+ datasets[geoptima.imei] ||= Geoptima::Dataset.new(geoptima.imei, options)
302
+ datasets[geoptima.imei] << geoptima
303
+ end
304
+ end
305
+ datasets
306
+ end
307
+
308
+ end
309
+
310
+ end
311
+
@@ -0,0 +1,3 @@
1
+ module Geoptima
2
+ VERSION = "0.0.1"
3
+ end
data/lib/geoptima.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'geoptima/version.rb'
2
+ require 'geoptima/data.rb'
3
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: geoptima
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Craig Taverner
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-01 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: |
22
+ Geoptima is a suite of applications for measuring and locating mobile/cellular subscriber experience on GPS enabled smartphones.
23
+ It is produced by AmanziTel AB in Helsingborg, Sweden, and supports many phone manufacturers, with free downloads from the
24
+ various app stores, markets or marketplaces. This Ruby library is only capable of reading the JSON format files priduced by these phones
25
+ and reformating them as CSV for further analysis in Excel. This is a simple and independent way of analysing the data, when
26
+ compared to the full-featured analysis applications and servers available from AmanziTel. If you want to analyse a limited amount
27
+ of data in excel, or with Ruby, then this GEM might be for you. If you want to analyse large amounts of data, from many subscribers, or over long periods of time
28
+ then rather consider the NetView and Customer IQ applications from AmanziTel at www.amanzitel.com.
29
+
30
+ email: craig@amanzi.com
31
+ executables:
32
+ - show_geoptima
33
+ extensions: []
34
+
35
+ extra_rdoc_files:
36
+ - README.rdoc
37
+ files:
38
+ - bin/show_geoptima
39
+ - lib/geoptima/version.rb
40
+ - lib/geoptima/data.rb
41
+ - lib/geoptima.rb
42
+ - examples/show_geoptima_sos.rb
43
+ - examples/export_layer.rb
44
+ - examples/stats.rb
45
+ - examples/show_geoptima.rb
46
+ - examples/sample_geoptima.json
47
+ - README.rdoc
48
+ - CHANGELOG
49
+ - CONTRIBUTORS
50
+ - Gemfile
51
+ - geoptima.gemspec
52
+ homepage: http://github.com/craigtaverner/geoptima.rb
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --quiet
58
+ - --title
59
+ - geoptima.rb
60
+ - --opname
61
+ - index.html
62
+ - --line-numbers
63
+ - --main
64
+ - README.rdoc
65
+ - --inline-source
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 57
74
+ segments:
75
+ - 1
76
+ - 8
77
+ - 7
78
+ version: 1.8.7
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ requirements: []
89
+
90
+ rubyforge_project: geoptima
91
+ rubygems_version: 1.7.2
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: Ruby access to Geoptima JSON files
95
+ test_files: []
96
+