where_was_i 0.0.1 → 0.0.3

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.
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