where_was_i 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f76b6b51ae66a959ee39162517a749fe40397d2c
4
- data.tar.gz: 639a75c546b03ef5c17c13f7b15db1112b231e7a
3
+ metadata.gz: af13af41cb05d18d3084de41b04c6876dccffbe5
4
+ data.tar.gz: 3aa33bfa697733a7ed454e882797c4cd52e747c8
5
5
  SHA512:
6
- metadata.gz: 3f1280bc1b60fd733398f0c011c6bfbb57e72eca637ade33681628232234d4f1916c1ec2f638c78015a2f29d645dc64a97a830af27640f49ec6448f6b1b52ad2
7
- data.tar.gz: 74209c3e5c8ab8296bcf86f78942a6e81f2253eeb7ef5aaa4158377056d54f6896af66f7bdff457cf1c252e1ae11d602f958297db1e1c95df7a254ee26066963
6
+ metadata.gz: 282b3e85109110fd42920f84c7b37763995a879e1a6b7729745e41ce64ef569556d7c43d83fee7c011aac7704d382ab5c33f1322727643b9a080376dba89d58e
7
+ data.tar.gz: 381154a92ce76bb626110d56768fb2a7250402c2069d4b1a84e812c0956dfe59cdc4a899c65f1cfe9a8e9508378f2b265abe410d88723b854df1e3f13d773c42
data/README.md CHANGED
@@ -2,15 +2,55 @@
2
2
 
3
3
  Given a GPX data file and a time reference, infer a location.
4
4
 
5
+ [API documentation](http://rubydoc.info/github/alexdean/where_was_i/master/frames) is available on rubydoc.org.
6
+
7
+ ## Examples
8
+
5
9
  ```ruby
6
10
  w = WhereWasI::Gpx.new(gpx_file: '/home/alex/track.gpx')
7
11
  w.at('2014-01-01T00:00:00Z')
8
12
  #=> {lat: 48.0, lon: 98.0, elevation: 1000}
9
13
  ```
10
14
 
11
- `at` will return `nil` if the supplied time is not covered by the GPX data.
15
+ By default, `at` will return `nil` if the supplied time is not covered by the GPX data.
16
+ This includes times before the earliest data or after the last data, or times that
17
+ fall in-between GPX segments. (Like if your GPS receiver was turned off for a few
18
+ minutes.)
19
+
12
20
  ```ruby
13
21
  w = WhereWasI::Gpx.new(gpx_file: '/home/alex/track.gpx')
14
22
  w.at('2014-01-02T00:00:00Z')
15
23
  #=> nil
16
24
  ```
25
+
26
+ ## Inter-segment Behavior
27
+
28
+ For times that fall outside any segments, you can opt to guess about a location
29
+ in a few different ways, instead of returning `nil`.
30
+
31
+ ### Interpolation
32
+
33
+ If you would like to interpolate a location using the ending and beginning
34
+ locations of the nearest segments, do the following:
35
+
36
+ ```ruby
37
+ w = WhereWasI::Gpx.new(
38
+ gpx_file: '/home/alex/track.gpx',
39
+ intersegment_behavior: :interpolate
40
+ )
41
+ w.at('2014-01-02T00:00:00Z')
42
+ ```
43
+
44
+ ### Nearest
45
+
46
+ Interpolating will give impossible results if a large distance exists between
47
+ two segments. In those cases, simply selecting the location of segment begin/end
48
+ which is nearest in time may be preferable.
49
+
50
+ ```ruby
51
+ w = WhereWasI::Gpx.new(
52
+ gpx_file: '/home/alex/track.gpx',
53
+ intersegment_behavior: :nearest
54
+ )
55
+ w.at('2014-01-02T00:00:00Z')
56
+ ```
@@ -4,16 +4,21 @@ module WhereWasI
4
4
 
5
5
  # Use a GPX file as a data source for location inferences.
6
6
  class Gpx
7
- attr_reader :tracks
7
+ attr_reader :tracks, :intersegment_behavior
8
+
9
+ Infinity = 1.0/0
8
10
 
9
11
  # @param [String] gpx_file Path to a GPX file.
10
12
  # @param [String] gpx_data GPX XML data string.
13
+ # @param [nil,Symbol] intersegment_behavior How to handle times that fall between track segments.
14
+ # nil: return nil
15
+ # :interpolate: Interpolate a location from the ends of the nearest segments
11
16
  #
12
17
  # @example with a gpx file
13
18
  # g = WhereWasI::Gpx.new(gpx_file: '/path/to/data.gpx')
14
19
  # @example with gpx data
15
20
  # g = WhereWasI::Gpx.new(gpx_data: '<?xml version="1.0"><gpx ...')
16
- def initialize(gpx_file:nil, gpx_data:nil)
21
+ def initialize(gpx_file:nil, gpx_data:nil, intersegment_behavior:nil)
17
22
  if gpx_file
18
23
  @gpx_data = open(gpx_file)
19
24
  elsif gpx_data
@@ -22,6 +27,12 @@ module WhereWasI
22
27
  raise ArgumentError, "Must supply gpx_file or gpx_data."
23
28
  end
24
29
 
30
+ valid_values = [nil, :interpolate, :nearest]
31
+ if !valid_values.include?(intersegment_behavior)
32
+ raise ArgumentError, "intersegment_behavior must be one of: #{valid_values.inspect}"
33
+ end
34
+ @intersegment_behavior = intersegment_behavior
35
+
25
36
  @tracks_added = false
26
37
  end
27
38
 
@@ -47,21 +58,97 @@ module WhereWasI
47
58
  end
48
59
  @tracks << track
49
60
  end
61
+
62
+ @intersegments = []
63
+ @tracks.each_with_index do |track,i|
64
+ next if i == 0
65
+
66
+ this_track = track
67
+ prev_track = @tracks[i-1]
68
+
69
+ inter_track = Track.new
70
+ inter_track.add_point(
71
+ lat: prev_track.end_location[0],
72
+ lon: prev_track.end_location[1],
73
+ elevation: prev_track.end_location[2],
74
+ time: prev_track.end_time
75
+ )
76
+ inter_track.add_point(
77
+ lat: this_track.start_location[0],
78
+ lon: this_track.start_location[1],
79
+ elevation: this_track.start_location[2],
80
+ time: this_track.start_time
81
+ )
82
+ @intersegments << inter_track
83
+ end
84
+
50
85
  @tracks_added = true
51
86
  end
52
87
 
53
88
  # infer a location from track data and a time
54
89
  #
55
- # @param [Time,String] time
90
+ # @param [Time,String,Fixnum] time
56
91
  # @return [Hash]
57
92
  # @see Track#at
58
93
  def at(time)
59
94
  add_tracks if ! @tracks_added
95
+
96
+ if time.is_a?(String)
97
+ time = Time.parse(time)
98
+ end
99
+ time = time.to_i
100
+
60
101
  location = nil
102
+
61
103
  @tracks.each do |track|
62
104
  location = track.at(time)
63
105
  break if location
64
106
  end
107
+
108
+ if ! location
109
+ case @intersegment_behavior
110
+ when :interpolate then
111
+ @intersegments.each do |track|
112
+ location = track.at(time)
113
+ break if location
114
+ end
115
+ when :nearest then
116
+ # hash is sorted in ascending time order.
117
+ # all start/end points for all segments
118
+ points = {}
119
+ @tracks.each do |t|
120
+ points[t.start_time.to_i] = t.start_location
121
+ points[t.end_time.to_i] = t.end_location
122
+ end
123
+
124
+ last_diff = Infinity
125
+ last_time = -1
126
+ points.each do |p_time,p_location|
127
+ this_diff = (p_time.to_i - time).abs
128
+
129
+ # as long as the differences keep getting smaller, we keep going
130
+ # as soon as we see a larger one, we step back one and use that value.
131
+ if this_diff > last_diff
132
+ l = points[last_time]
133
+ location = Track.array_to_hash(points[last_time])
134
+ break
135
+ else
136
+ last_diff = this_diff
137
+ last_time = p_time
138
+ end
139
+ end
140
+
141
+ # if we got here, time is > the end of the last segment
142
+ location = Track.array_to_hash(points[last_time])
143
+ end
144
+ end
145
+
146
+ # each segment has a begin and end time.
147
+ # which one is this time closest to?
148
+ # array of times in order. compute abs diff between time and each point.
149
+ # put times in order. abs diff to each, until we get a larger value or we run out of points. then back up one and use that.
150
+ # {time => [lat, lon, elev], time => [lat, lon, elev]}
151
+
65
152
  location
66
153
  end
67
154
  end
@@ -5,7 +5,15 @@ module WhereWasI
5
5
 
6
6
  # a series of sequential [lat, lon, elevation] points
7
7
  class Track
8
- attr_reader :start_time, :end_time
8
+ attr_reader :start_time, :end_time, :start_location, :end_location
9
+
10
+ def self.array_to_hash(a)
11
+ {
12
+ lat: a[0],
13
+ lon: a[1],
14
+ elevation: a[2]
15
+ }
16
+ end
9
17
 
10
18
  def initialize
11
19
  @points = {}
@@ -20,9 +28,19 @@ module WhereWasI
20
28
  def add_point(lat:, lon:, elevation:, time:)
21
29
  time = Time.parse(time) if ! time.is_a?(Time)
22
30
 
23
- @start_time = time if @start_time.nil? || time < @start_time
24
- @end_time = time if @end_time.nil? || time > @end_time
25
- @points[time.to_i] = [lat, lon, elevation]
31
+ current = [lat, lon, elevation]
32
+
33
+ if @start_time.nil? || time < @start_time
34
+ @start_time = time
35
+ @start_location = current
36
+ end
37
+
38
+ if @end_time.nil? || time > @end_time
39
+ @end_time = time
40
+ @end_location = current
41
+ end
42
+
43
+ @points[time.to_i] = current
26
44
 
27
45
  true
28
46
  end
@@ -48,23 +66,23 @@ module WhereWasI
48
66
  #
49
67
  # @example
50
68
  # track.at(time) => {lat:48, lon:98, elevation: 2100}
51
- # @param time [String,Time]
69
+ # @param time [String,Time,Fixnum]
52
70
  # @return [Hash,nil]
53
71
  def at(time)
54
- return nil if ! in_time_range?(time)
55
- if ! time.is_a?(Time)
56
- time = Time.parse(time).to_i
72
+ if time.is_a?(String)
73
+ time = Time.parse(time)
57
74
  end
58
- time = time.to_i
75
+ if time.is_a?(Fixnum)
76
+ time = Time.at(time)
77
+ end
78
+ raise ArgumentError, "time must be a Time,String, or Fixnum" if ! time.is_a?(Time)
79
+
80
+ return nil if ! in_time_range?(time)
59
81
 
60
82
  @interp ||= Interpolate::Points.new(@points)
61
- data = @interp.at(time)
83
+ data = @interp.at(time.to_i)
62
84
 
63
- {
64
- lat: data[0],
65
- lon: data[1],
66
- elevation: data[2]
67
- }
85
+ self.class.array_to_hash(data)
68
86
  end
69
87
  end
70
88
 
@@ -1,3 +1,3 @@
1
1
  module WhereWasI
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -47,5 +47,51 @@ RSpec.describe WhereWasI::Gpx do
47
47
  expect(subject.at('2014-06-17T12:00:00Z')).to eq nil
48
48
  end
49
49
 
50
+ it "should do inter-track interpolation" do
51
+ subject = WhereWasI::Gpx.new(gpx_file: test_filename, intersegment_behavior: :interpolate)
52
+ expect(subject.at('2014-06-17T12:00:00Z')).to eq({
53
+ elevation: 187.56424463380702,
54
+ lat: 48.75755751944594,
55
+ lon: -87.60709448942973
56
+ })
57
+ end
58
+
59
+ describe "nearest behavior" do
60
+ let(:subject) {WhereWasI::Gpx.new(gpx_file: test_filename, intersegment_behavior: :nearest)}
61
+
62
+ it "should return start of first segment for a time earlier than the first segment" do
63
+ expect(subject.intersegment_behavior).to eq :nearest
64
+ expect(subject.at('2014-06-16T16:00:00Z')).to eq({
65
+ lat: 48.833804307505488,
66
+ lon: -87.519973134621978,
67
+ elevation: 186.150000000000006
68
+ })
69
+ end
70
+
71
+ it "should return the end of the last segment for a time after the end of the last segment" do
72
+ expect(subject.at('2014-06-17T15:00:00Z')).to eq({
73
+ lat: 48.747312789782882,
74
+ lon: -87.618932314217091,
75
+ elevation: 188.080000000000013
76
+ })
77
+ end
78
+
79
+ it "should return the closest segment beginning when between segments but nearer to a begin" do
80
+ expect(subject.at('2014-06-16T17:00:00Z')).to eq({
81
+ lat: 48.833690145984292,
82
+ lon: -87.520155273377895,
83
+ elevation: 183.75
84
+ })
85
+ end
86
+
87
+ it "should return the closest segment ending when between segments but nearer to an end" do
88
+ expect(subject.at('2014-06-17T13:00:00Z')).to eq({
89
+ lat: 48.747263001278043,
90
+ lon: -87.618850255385041,
91
+ elevation: 188.080000000000013
92
+ })
93
+ end
94
+ end
95
+
50
96
  end
51
97
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: where_was_i
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dean
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-14 00:00:00.000000000 Z
11
+ date: 2014-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri