geoptima 0.1.15 → 0.1.17

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/lib/geoptima/data.rb CHANGED
@@ -4,6 +4,7 @@ require 'rubygems'
4
4
  require 'multi_json'
5
5
  require 'geoptima/daterange'
6
6
  require 'geoptima/locationrange'
7
+ require 'geoptima/timer'
7
8
  begin
8
9
  require 'png'
9
10
  rescue LoadError
@@ -40,24 +41,43 @@ module Geoptima
40
41
  end
41
42
 
42
43
  class Trace
43
- attr_reader :dataset, :data_id, :data_ids, :name, :tracename
44
+ attr_reader :dataset, :data_id, :data_id_hashset, :name, :tracename
44
45
  attr_reader :bounds, :events, :totals, :scale, :padding
45
- def initialize(dataset)
46
+ def initialize(dataset, options={})
46
47
  @dataset = dataset
48
+ @trace_type = options[:type]
47
49
  @data_id = nil
48
- @data_ids = []
49
- @name = dataset.name
50
+ @data_id_hashset = {}
51
+ @name = options[:name] || dataset.name
50
52
  @events = []
53
+ case @trace_type
54
+ when /ways/
55
+ @ptag = 'wpt'
56
+ when /route/
57
+ @ttag = 'rte'
58
+ @ptag = 'rtept'
59
+ else
60
+ @ttag = 'trk'
61
+ @stag = 'trkseg'
62
+ @ptag = 'trkpt'
63
+ end
64
+ end
65
+ def data_ids
66
+ puts "About to sort data ids: #{@data_id_hashset.keys.join(',')}" if($debug)
67
+ @data_id_hashset.keys.compact.sort
51
68
  end
52
69
  def <<(e)
53
- @tracename ||= "#{name}-#{e.time}"
54
- unless @data_id
55
- @data_id ||= e.file.id
56
- @data_ids << e.file.id
70
+ if gpx_id = e.gpx_id
71
+ @tracename ||= "#{name}-#{e.time}"
72
+ @data_id ||= e.gpx_id
73
+ @data_id_hashset[e.gpx_id] ||= 0
74
+ @data_id_hashset[e.gpx_id] += 1
75
+ check_bounds(e)
76
+ check_totals(e)
77
+ @events << e unless(co_located(e,@events[-1]))
78
+ elsif $debug
79
+ puts "Ignoring event with nil GPX id: #{e.inspect}"
57
80
  end
58
- check_bounds(e)
59
- check_totals(e)
60
- @events << e unless(co_located(e,@events[-1]))
61
81
  end
62
82
  def co_located(event,other)
63
83
  event && other && event.latitude == other.latitude && event.longitude == other.longitude
@@ -88,13 +108,22 @@ module Geoptima
88
108
  raise "Passed a string value: #{value.inspect}"
89
109
  end
90
110
  begin
91
- @bounds[key] = value if(@bounds[key].nil? || @bounds[key] > value)
111
+ @bounds[key] = value if(!value.nil? &&(@bounds[key].nil? || @bounds[key] > value))
92
112
  rescue
93
113
  raise "Failed to set bounds using current:#{@bounds.inspect}, value=#{value.inspect}"
94
114
  end
95
115
  end
96
116
  def check_bounds_max(key,value)
97
- @bounds[key] = value if(@bounds[key].nil? || @bounds[key] < value)
117
+ @bounds[key] = value if(!value.nil? &&(@bounds[key].nil? || @bounds[key] < value))
118
+ end
119
+ def check_trace_bounds(t)
120
+ @bounds ||= {}
121
+ check_bounds_min :minlat, t.bounds[:minlat]
122
+ check_bounds_min :minlon, t.bounds[:minlon]
123
+ check_bounds_max :maxlat, t.bounds[:maxlat]
124
+ check_bounds_max :maxlon, t.bounds[:maxlon]
125
+ @width = nil
126
+ @height = nil
98
127
  end
99
128
  def each
100
129
  events.each{|e| yield e}
@@ -174,10 +203,57 @@ module Geoptima
174
203
  def bounds_as_gpx
175
204
  "<bounds " + bounds.keys.map{|k| "#{k}=\"#{bounds[k]}\""}.join(' ') + "/>"
176
205
  end
177
- def event_as_gpx(e,index)
178
- "<trkpt lat=\"#{e.latitude}\" lon=\"#{e.longitude}\"><ele>#{index}</ele><time>#{e.time}</time></trkpt>"
206
+ def event_as_gpx(e,elevation=0.0)
207
+ "<#{@ptag} lat=\"#{e.latitude}\" lon=\"#{e.longitude}\"><ele>#{elevation}</ele><time>#{e.time}</time><name>#{e.name}</name><desc>#{e.description}</desc></#{@ptag}>"
179
208
  end
180
209
  def as_gpx
210
+ return unless(length>0)
211
+ case @trace_type
212
+ when /way/
213
+ as_gpx_ways
214
+ when /route/
215
+ as_gpx_route
216
+ else
217
+ as_gpx_track
218
+ end
219
+ end
220
+ def as_gpx_ways
221
+ gpx = %{<?xml version="1.0" encoding="UTF-8"?>
222
+ <gpx version="1.1" creator="geoptima.rb - Craig Taverner">
223
+ <metadata>
224
+ #{bounds_as_gpx}
225
+ </metadata>
226
+ <name>#{tracename}</name>
227
+ } +
228
+ traces.map do |trace|
229
+ trace.events.map do |e|
230
+ event_as_gpx(e)
231
+ end.join("\n ")
232
+ end.join("\n ") +
233
+ """
234
+ </gpx>
235
+ """
236
+ end
237
+ def as_gpx_route
238
+ gpx = %{<?xml version="1.0" encoding="UTF-8"?>
239
+ <gpx version="1.1" creator="geoptima.rb - Craig Taverner">
240
+ <metadata>
241
+ #{bounds_as_gpx}
242
+ </metadata>
243
+ <rte>
244
+ <name>#{tracename}</name>
245
+ } +
246
+ traces.map do |trace|
247
+ trace.events.map do |e|
248
+ event_as_gpx(e,ei)
249
+ end.join("\n ")
250
+ end.join("\n ") +
251
+ """
252
+ </rte>
253
+ </gpx>
254
+ """
255
+ end
256
+ def as_gpx_track
181
257
  ei = 0
182
258
  gpx = %{<?xml version="1.0" encoding="UTF-8"?>
183
259
  <gpx version="1.1" creator="geoptima.rb - Craig Taverner">
@@ -193,7 +269,7 @@ module Geoptima
193
269
  ei += 1
194
270
  event_as_gpx(e,ei)
195
271
  end.join("\n ")
196
- end.join("\n </trkseg><trkseg>") +
272
+ end.join("\n </trkseg>\n <trkseg>\n ") +
197
273
  """
198
274
  </trkseg>
199
275
  </trk>
@@ -217,6 +293,13 @@ module Geoptima
217
293
  end
218
294
  options[key] = val
219
295
  end
296
+ puts "Fixed options: #{options.inspect}"
297
+ options['show_line'].nil? && (options['show_line']=true)
298
+ if @trace_type =~ /ways/
299
+ options['show_line'] = false
300
+ options['point_size'] = (options['point_size'].to_i + 1) * 2
301
+ puts "Adjusted line/point settings for ways: show_line:#{options['show_line']}, point_size:#{options['point_size']}"
302
+ end
220
303
  end
221
304
  def traces
222
305
  [self]
@@ -245,14 +328,15 @@ module Geoptima
245
328
  end
246
329
 
247
330
  data_idm = data_ids.inject({}){|a,v| a[v]=a.length;a}
331
+ puts "Created map of data ids from available set: #{data_ids.inspect}"
248
332
  if data_ids.length > 1
249
333
  data_ids.each do |did|
250
- puts "Mapped data ID '#{did}' to color: #{color(data_idm[did])}"
334
+ puts "\t#{color(data_idm[did])}\t#{data_id_hashset[did]}\t#{did}"
251
335
  end
252
336
  end
253
337
  traces.each_with_index do |trace,index|
254
- point_color = color(index)
255
338
  line_color = PNG::Color.from "0x00006688"
339
+ point_color = color(index)
256
340
  if options['point_color'] && !(options['point_color'] =~ /auto/i)
257
341
  pc = options['point_color'].gsub(/^\#/,'').gsub(/^0x/,'').upcase
258
342
  pc = "0x#{pc}FFFFFFFF"[0...10]
@@ -261,9 +345,6 @@ module Geoptima
261
345
  rescue
262
346
  puts "Failed to interpret color #{pc}, use format 0x00000000: #{$!}"
263
347
  end
264
- elsif data_ids.length > 1
265
- point_color = color(data_idm[trace.data_id])
266
- puts "Got point color #{point_color} from data ID '#{trace.data_id}' index #{data_idm[trace.data_id]}" if($debug)
267
348
  else
268
349
  point_color = color(index)
269
350
  puts "Got point color #{point_color} from trace index #{index}" if($debug)
@@ -272,9 +353,13 @@ module Geoptima
272
353
  prev = nil
273
354
  trace.events.each do |e|
274
355
  p = scale_event(e)
356
+ if data_idm.length > 1
357
+ point_color = color(data_idm[e.gpx_id])
358
+ puts "Got point color #{point_color} from data ID '#{e.gpx_id}' index #{data_idm[e.gpx_id]}" if($debug)
359
+ end
275
360
  begin
276
361
  # draw an anti-aliased line
277
- if prev && prev != p
362
+ if options['show_line'] && prev && prev != p
278
363
  canvas.line prev[0], prev[1], p[0], p[1], line_color
279
364
  end
280
365
 
@@ -306,7 +391,7 @@ module Geoptima
306
391
  def initialize(dataset)
307
392
  @dataset = dataset
308
393
  @name = dataset.name
309
- @data_ids = []
394
+ @data_id_hashset = {}
310
395
  @traces = []
311
396
  end
312
397
  def tracename
@@ -315,7 +400,7 @@ module Geoptima
315
400
  def <<(t)
316
401
  check_trace_totals(t)
317
402
  check_trace_bounds(t)
318
- @data_ids = (@data_ids+t.data_ids).sort.uniq.compact
403
+ @data_id_hashset = @data_id_hashset.merge(t.data_id_hashset)
319
404
  @traces << t
320
405
  end
321
406
  def each
@@ -332,15 +417,6 @@ module Geoptima
332
417
  @totals ||= [0.0,0.0,0]
333
418
  [0,1,2].each{|i| @totals[i] += t.totals[i]}
334
419
  end
335
- def check_trace_bounds(t)
336
- @bounds ||= {}
337
- check_bounds_min :minlat, t.bounds[:minlat]
338
- check_bounds_min :minlon, t.bounds[:minlon]
339
- check_bounds_max :maxlat, t.bounds[:maxlat]
340
- check_bounds_max :maxlon, t.bounds[:maxlon]
341
- @width = nil
342
- @height = nil
343
- end
344
420
  end
345
421
 
346
422
  module ErrorCounter
@@ -464,8 +540,14 @@ module Geoptima
464
540
  def closer_than(other,seconds=60)
465
541
  (self - other).abs < seconds
466
542
  end
543
+ def latitude
544
+ @latitude ||= self['latitude']
545
+ end
546
+ def longitude
547
+ @longitude ||= self['longitude']
548
+ end
467
549
  def location
468
- @location ||= self['latitude'] && Point.new(self['latitude'],self['longitude'])
550
+ @location ||= latitude && Point.new(latitude,longitude)
469
551
  end
470
552
  def locate(gps)
471
553
  incr_error "GPS String Data" if(gps['latitude'].is_a? String)
@@ -482,6 +564,110 @@ module Geoptima
482
564
  def to_s
483
565
  "#{name}[#{time}]: #{@fields.inspect}"
484
566
  end
567
+ def description
568
+ "#{name}"
569
+ end
570
+ def valid_gpx?
571
+ location
572
+ end
573
+ def gpx_id
574
+ file.id
575
+ end
576
+ def to_type
577
+ case name
578
+ when 'runningApps'
579
+ RunningApps.new(self)
580
+ else
581
+ self
582
+ end
583
+ end
584
+ end
585
+
586
+ class AppCategory
587
+ attr_reader :key, :category, :app_class, :name
588
+ def initialize(key, category, app_class, name)
589
+ @key, @category, @app_class, @name = key, category, app_class, name
590
+ end
591
+ end
592
+
593
+ class AppCategories
594
+ attr_reader :app_categories
595
+ def initialize(path)
596
+ begin
597
+ @app_categories = File.open(path).inject({}) do |a,l|
598
+ f=l.chomp.split(/\,/).map{|v| v.gsub(/^\s+/,'').gsub(/\s+$/,'')}
599
+ a[f[0]] = AppCategory.new(*f) unless(f[0] =~ /appName.unique/i)
600
+ a
601
+ end
602
+ rescue
603
+ puts "Failed to load app categories from '#{path}': #{$!}"
604
+ @app_categories = {}
605
+ end
606
+ end
607
+ def length
608
+ @app_categories.length
609
+ end
610
+ def [](key)
611
+ @app_categories[key]
612
+ end
613
+ def apps
614
+ @apps ||= @app_categories.keys.compact.sort.uniq
615
+ end
616
+ def categories
617
+ @categories ||= @app_categories.values.map{|c| c.category}.compact.sort.uniq
618
+ end
619
+ def app_classes
620
+ @app_classes ||= @app_categories.values.map{|c| c.app_class}.compact.sort.uniq
621
+ end
622
+ def to_s
623
+ "#{length} apps, #{categories.length} categories, #{app_classes.length} classes"
624
+ end
625
+ def a_to_s(a)
626
+ (a.length > 3 ? a[0..2]+['...'] : a).join(',')
627
+ end
628
+ def describe
629
+ "#{length} apps, #{categories.length} categories (#{a_to_s(categories)}), #{app_classes.length} classes (#{a_to_s(app_classes)})"
630
+ end
631
+ end
632
+
633
+ # This class allows the 'runningApps' event to behave differently than
634
+ # normal events. It is used by the GPX exporter to color code these based
635
+ # on the app category
636
+ class RunningApps
637
+ attr_reader :inner_event
638
+ def initialize(event)
639
+ @inner_event = event
640
+ end
641
+ def method_missing(symbol,*args,&block)
642
+ @inner_event.send symbol, *args, &block
643
+ end
644
+ def description
645
+ @inner_event['appName']
646
+ end
647
+ def valid_gpx?
648
+ @inner_event.valid_gpx? && @inner_event['state'] == 'STARTED'
649
+ end
650
+ def gpx_id
651
+ app = @inner_event['appName']
652
+ if $app_categories && $app_categories.length > 0
653
+ puts "Choosing internal GPX ID based on specified categories: #{$app_categories.describe}" if($debug)
654
+ if $app_categories.categories.length == 1
655
+ puts "Filtering on only one category, using app name for GPX ID: #{app}" if($debug)
656
+ if (a=$app_categories[app]) && a.category =~ /\w/
657
+ a.key
658
+ else
659
+ nil
660
+ end
661
+ elsif (a=$app_categories[app]) && a.category =~ /\w/
662
+ puts "Found matching category app[#{app}] => #{a.category}" if($debug)
663
+ a.category
664
+ else
665
+ nil
666
+ end
667
+ else
668
+ app
669
+ end
670
+ end
485
671
  end
486
672
 
487
673
  # The Geoptima::Data is an entire JSON file of events
@@ -721,6 +907,8 @@ module Geoptima
721
907
 
722
908
  def recent(event,key,seconds=60)
723
909
  unless event[key]
910
+ timer("export.event.recent").start
911
+ timer("export.event.recent.#{key}").start
724
912
  if imei = event.file.imei
725
913
  puts "Searching for recent values for '#{key}' starting at event #{event}" if($debug)
726
914
  ev,prop=key.split(/\./)
@@ -742,6 +930,8 @@ module Geoptima
742
930
  else
743
931
  puts "Not searching for correlated data without imei: #{event}"
744
932
  end
933
+ timer("export.event.recent.#{key}").stop
934
+ timer("export.event.recent").stop
745
935
  end
746
936
  # @recent[key] ||= ''
747
937
  event[key]
@@ -780,9 +970,11 @@ module Geoptima
780
970
  def sorted(event_type=nil)
781
971
  merge_events unless @sorted
782
972
  unless @sorted[event_type] || event_type.nil?
973
+ timer("sorted.#{event_type}").start
783
974
  @sorted[event_type] = @sorted[nil].reject do |event|
784
975
  event.name != event_type
785
976
  end
977
+ timer("sorted.#{event_type}").stop
786
978
  end
787
979
  @sorted[event_type]
788
980
  end
@@ -797,6 +989,7 @@ module Geoptima
797
989
  def stats
798
990
  merge_events unless @sorted
799
991
  unless @stats
992
+ timer('stats').start
800
993
  @stats = {}
801
994
  event_count = 0
802
995
  sorted.each do |event|
@@ -809,6 +1002,7 @@ module Geoptima
809
1002
  @stats[key][value] += 1
810
1003
  end
811
1004
  end
1005
+ timer('stats').stop
812
1006
  end
813
1007
  @stats.reject! do |k,v|
814
1008
  v.length > 500 || v.length > 10 && v.length > event_count / 2
@@ -820,9 +1014,23 @@ module Geoptima
820
1014
  @data.map{ |v| v.events_names }.flatten.uniq.sort
821
1015
  end
822
1016
 
1017
+ def timer(name)
1018
+ @timers ||= {}
1019
+ @timers[name] ||= Geoptima::Timer.new(name)
1020
+ end
1021
+
1022
+ def dump_timers(out=STDOUT)
1023
+ out.puts "Printing timer information for #{@timers.length} timers:"
1024
+ @timers.keys.sort.each do |key|
1025
+ t = @timers[key]
1026
+ out.puts "\t#{t.describe}"
1027
+ end
1028
+ end
1029
+
823
1030
  def merge_events
824
1031
  @sorted ||= {}
825
1032
  unless @sorted[nil]
1033
+ timer('merge_events').start
826
1034
  event_hash = {}
827
1035
  puts "Creating sorted maps for #{self}" if($debug)
828
1036
  events_names.each do |name|
@@ -845,16 +1053,34 @@ module Geoptima
845
1053
  puts "After adding #{name} events, maps are #{event_hash.length} long" if($debug)
846
1054
  end
847
1055
  puts "Merging and sorting #{event_hash.keys.length} maps" if($debug)
1056
+ timer('merge_events.sort').start
848
1057
  @sorted[nil] = event_hash.keys.sort.map{|k| event_hash[k]}
1058
+ timer('merge_events.sort').stop
849
1059
  puts "Sorted #{@sorted[nil].length} events" if($debug)
1060
+ timer('merge_events.locate').start
850
1061
  locate_events if(options[:locate])
1062
+ timer('merge_events.locate').stop
1063
+ timer('merge_events').stop
851
1064
  end
852
1065
  @sorted
853
1066
  end
854
1067
 
1068
+ def waypoints(waypoints=nil)
1069
+ @waypoints ||= {}
1070
+ event_type = waypoints=='all' ? nil : waypoints
1071
+ unless @waypoints[event_type]
1072
+ @waypoints[event_type] = Trace.new(self, :type => 'ways', :name => "waypoints-#{self.name}")
1073
+ sorted(event_type).each do |e|
1074
+ e = e.to_type
1075
+ @waypoints[event_type] << e if(e.valid_gpx?)
1076
+ end
1077
+ end
1078
+ @waypoints[event_type]
1079
+ end
1080
+
855
1081
  def each_trace
856
- puts "Exporting GPX traces"
857
1082
  trace = nil
1083
+ timer('each_trace').start
858
1084
  sorted('gps').each do |gps|
859
1085
  trace ||= Trace.new(self)
860
1086
  if trace.too_far(gps)
@@ -864,19 +1090,22 @@ module Geoptima
864
1090
  trace << gps
865
1091
  end
866
1092
  yield trace if(trace)
1093
+ timer('each_trace').stop
867
1094
  end
868
1095
 
869
1096
  def locate_events
870
1097
  prev_gps = nil
871
1098
  count = 0
872
- puts "Locating #{sorted.length} events" if($debug)
1099
+ puts "Locating #{sorted.length} events" if(true||$debug)
873
1100
  sorted.each do |event|
1101
+ timer('locate.each').start
874
1102
  if event.name === 'gps'
875
1103
  event.locate(event)
876
1104
  prev_gps = event
877
1105
  elsif prev_gps
878
1106
  count += 1 if(event.locate_if_closer_than(prev_gps,60))
879
1107
  end
1108
+ timer('locate.each').stop
880
1109
  end
881
1110
  puts "Located #{count} / #{sorted.length} events" if($debug)
882
1111
  end
@@ -8,8 +8,8 @@ module Geoptima
8
8
  HUNDRED_YEARS_SECONDS = 100 * 365 * DAY_SECONDS
9
9
  HUNDRED_YEARS_MILLIS = HUNDRED_YEARS_SECONDS * 1000
10
10
  def self.from_file(arg)
11
- base,time=arg.to_s.split(/_/)
12
- self.from(time)
11
+ base,*times=arg.to_s.split(/_/)
12
+ times.map{|time| self.from(time)}
13
13
  end
14
14
  def self.from(time)
15
15
  time = time.to_f
@@ -28,3 +28,4 @@ if $PROGRAM_NAME =~ /\/file_time.rb$/
28
28
  puts "#{(filename.to_s+" "*40)[0..40]} --> #{Geoptima::FileTime.from_file filename}"
29
29
  end
30
30
  end
31
+
@@ -20,6 +20,19 @@ module Geoptima
20
20
  def <=(other)
21
21
  self.latitude - other.latitude <= 0 && self.longitude - other.longitude <= 0
22
22
  end
23
+ def -(other)
24
+ other.respond_to?('latitude') ?
25
+ Point.new(self.latitude - other.latitude, self.longitude - other.longitude) :
26
+ Point.new(self.latitude - other.to_f, self.longitude - other.to_f)
27
+ end
28
+ def +(other)
29
+ other.respond_to?('latitude') ?
30
+ Point.new(self.latitude + other.latitude, self.longitude + other.longitude) :
31
+ Point.new(self.latitude + other.to_f, self.longitude + other.to_f)
32
+ end
33
+ def distance(other)
34
+ Math.sqrt( (self.latitude-other.latitude)**2 + (self.longitude-other.longitude)**2 )
35
+ end
23
36
  def to_s
24
37
  [@latitude,@longitude].inspect
25
38
  end
@@ -28,14 +41,16 @@ module Geoptima
28
41
  class LocationRange
29
42
  attr_reader :min, :max
30
43
  def initialize(spec)
31
- f=spec.gsub(/\.\./,':').split(/[\,\;\:]/)
44
+ f=spec.gsub(/\.\./,':').gsub(/RANGE[\(\[]/i,'').split(/[\,\;\:]/)
32
45
  if spec =~ /\.\./
33
- @min = Point.new(f[0],f[2])
34
- @max = Point.new(f[1],f[3])
46
+ initialize_min_max(Point.new(f[0],f[2]), Point.new(f[1],f[3]))
35
47
  else
36
- @min = Point.new(f[0],f[1])
37
- @max = Point.new(f[2],f[3])
48
+ initialize_min_max(Point.new(f[0],f[1]), Point.new(f[2],f[3]))
38
49
  end
50
+ end
51
+ def initialize_min_max(min,max)
52
+ @min = min
53
+ @max = max
39
54
  if @min > @max
40
55
  p = @min
41
56
  @min = @max
@@ -52,6 +67,8 @@ module Geoptima
52
67
  def self.from(spec)
53
68
  if spec == '*' || spec =~ /everywhere/i
54
69
  LocationEverywhere.new
70
+ elsif spec =~ /dist[\(\[]\s*([\d\.\-\+]+)\s*\,\s*([\d\.\-\+]+)\s*\,\s*([\d\.\-\+]+)\s*[\)\]]/i
71
+ LocationDistance.new($1.to_f,Point.new($2,$3))
55
72
  else
56
73
  LocationRange.new(spec)
57
74
  end
@@ -63,8 +80,11 @@ module Geoptima
63
80
  [
64
81
  '56.1..57.0,12.0..15.8',
65
82
  '56.1,12.0,57.0,15.8',
83
+ 'range[56.1..57.0,12.0..15.8]',
84
+ 'range[56.1,12.0,57.0,15.8]',
66
85
  'everywhere',
67
- '*'
86
+ '*',
87
+ 'dist(70,56.5,12.0)'
68
88
  ].each do |test|
69
89
  puts "Testing: #{test}"
70
90
  range = Geoptima::LocationRange.from(test)
@@ -80,6 +100,25 @@ module Geoptima
80
100
  end
81
101
  end
82
102
  end
103
+ # Note that this class does the distance calculation based on a direct translation
104
+ # of distance at the equator. This will be inaccurate far from the equator.
105
+ class LocationDistance <LocationRange
106
+ attr_reader :distance, :distance_in_km, :center
107
+ def initialize(distance_in_km,center)
108
+ @distance_in_km = distance_in_km.to_f
109
+ @center = center
110
+ initialize_min_max(@center - distance, @center + distance)
111
+ end
112
+ def distance
113
+ @distance ||= distance_in_km * 360.0 / 40000.0
114
+ end
115
+ def include?(point)
116
+ super(point) && center.distance(point) < distance
117
+ end
118
+ def to_s
119
+ super.to_s+",(#{distance},#{center})"
120
+ end
121
+ end
83
122
  class LocationEverywhere <LocationRange
84
123
  def initialize()
85
124
  super("-90,90,-180,180")
@@ -0,0 +1,39 @@
1
+ module Geoptima
2
+
3
+ class Timer
4
+ attr_reader :name, :start_time, :end_time, :duration, :full_duration, :running
5
+ def initialize(name)
6
+ @name = name
7
+ reset
8
+ end
9
+ def reset
10
+ @running = false
11
+ @duration = 0
12
+ @full_duration = 0
13
+ @start_time = nil
14
+ @end_time = nil
15
+ end
16
+ def start
17
+ @duration = 0
18
+ @running = true
19
+ @start_time = Time.new
20
+ end
21
+ def stop
22
+ if running
23
+ @running = false
24
+ @end_time = Time.new
25
+ @duration = @end_time - @start_time
26
+ @full_duration += @duration
27
+ end
28
+ @duration
29
+ end
30
+ def to_s
31
+ "#{name}(#{full_duration}s)"
32
+ end
33
+ def describe
34
+ "#{name}\t#{full_duration}s"
35
+ end
36
+ end
37
+
38
+ end
39
+
@@ -1,6 +1,6 @@
1
1
  module Geoptima
2
2
 
3
- VERSION = "0.1.15"
3
+ VERSION = "0.1.17"
4
4
 
5
5
  class Version
6
6
  attr_reader :comparator, :version, :major, :minor, :patch