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 +4 -4
- data/README.md +41 -1
- data/lib/where_was_i/gpx.rb +90 -3
- data/lib/where_was_i/track.rb +33 -15
- data/lib/where_was_i/version.rb +1 -1
- data/spec/lib/where_was_i/gpx_spec.rb +46 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af13af41cb05d18d3084de41b04c6876dccffbe5
|
4
|
+
data.tar.gz: 3aa33bfa697733a7ed454e882797c4cd52e747c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
```
|
data/lib/where_was_i/gpx.rb
CHANGED
@@ -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
|
data/lib/where_was_i/track.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
25
|
-
@
|
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
|
-
|
55
|
-
|
56
|
-
time = Time.parse(time).to_i
|
72
|
+
if time.is_a?(String)
|
73
|
+
time = Time.parse(time)
|
57
74
|
end
|
58
|
-
|
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
|
|
data/lib/where_was_i/version.rb
CHANGED
@@ -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.
|
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-
|
11
|
+
date: 2014-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|