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