broutes 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NzQyYjM1NDdiMDY2Njk3ZmJiOGY0NGM5YmMwYjU3MGVlNGZmMmE4Mg==
5
+ data.tar.gz: !binary |-
6
+ YTJiZjQ3Nzk5NTA1YTE1NzYwODA2YTlkNjQzYmJiMDQxNTljYzM0Yg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ ZWRmMTYwMGY1OTQ1ODdhZWY2NjE1MzkyNmFjZDA5MzNlMjMxNmVkMzViMTA5
10
+ MGI4Yjk0ZTI2YTRkMjgwMDkxZjE5ZWZkMzE3MTQxN2M2YWU2OTAzYTNlMmQ4
11
+ YmYzM2JlOThiMTkxYzUzNmI2MjlmNDBmNmM5M2YzZWFkYmYzZjI=
12
+ data.tar.gz: !binary |-
13
+ Y2U5NmE5OWIyMzVhYzA3Y2FlMmNhN2M0MjA5YjdlYTU2NjVjMWQ2MmVkZGUw
14
+ NzQ4NWY0MTFmMDQwYzNlMjNjZTZlNzVhNTU0ZmNlOTZkNTBlMjZlY2ZkMTM1
15
+ Y2E5NjEzZWFiNGFmYmM5OTAzNDUxZDc0NDVkNGI4OGFhYTc5MDE=
@@ -0,0 +1,22 @@
1
+ # Public : provides factory method for loading appropriate route file parser
2
+ module Broutes::Formats
3
+ class Factory
4
+ # Public : factory method
5
+ #
6
+ # format - Symbol describing the format [:gpx_track, :tcx, :fit]
7
+ #
8
+ # Returns a route file parser
9
+ def get(format)
10
+ case format
11
+ when :gpx_track, 'application/gpx+xml', /\.gpx$/
12
+ GpxTrack.new
13
+ when :tcx, 'application/vnd.garmin.tcx+xml', /\.tcx$/
14
+ Tcx.new
15
+ when :fit, 'application/vnd.ant.fit', /\.fit$/
16
+ FitFile.new
17
+ else
18
+ raise ArgumentError.new("Unrecognised format #{format}. Supported formats are :gpx_track, :tcx, :fit")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ require 'fit'
2
+
3
+ module Broutes::Formats
4
+ class FitFile
5
+
6
+ def load(file, route)
7
+ fit_file = Fit::File.read(file)
8
+ Broutes.logger.info {"Started fit processing"}
9
+ i = 0
10
+ fit_file.records.select {|r| r.content && r.content.record_type == :record }.each do |r|
11
+ begin
12
+ pr = r.content
13
+ data = { time: record_time(r) }
14
+ data[:lat] = convert_position(pr.position_lat) if pr.respond_to?(:position_lat)
15
+ data[:lon] = convert_position(pr.position_long) if pr.respond_to?(:position_long)
16
+ data[:elevation] = pr.altitude if pr.respond_to?(:altitude)
17
+ [:distance, :heart_rate, :power, :speed, :cadence, :temperature].each do |m|
18
+ data[m] = pr.send(m) if pr.respond_to?(m)
19
+ end
20
+
21
+ route.add_point(data)
22
+ i += 1
23
+ rescue => e
24
+ Broutes.logger.debug {"#{e.message} for #{r}"}
25
+ end
26
+ end
27
+ Broutes.logger.info {"Loaded #{i} data points"}
28
+ end
29
+
30
+ def convert_position(value)
31
+ (8.381903171539307e-08 * value).round(5)
32
+ end
33
+
34
+ def record_time(record)
35
+ utc_seconds = record.content.timestamp
36
+ utc_seconds += record.header.time_offset if record.header.compressed_timestamp?
37
+ Time.new(1989, 12, 31) + utc_seconds
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ require 'nokogiri'
2
+
3
+ module Broutes::Formats
4
+ class GpxRoute
5
+ def load(file, route)
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ require 'nokogiri'
2
+
3
+ module Broutes::Formats
4
+ class GpxTrack
5
+
6
+ def load(file, route)
7
+ doc = Nokogiri::XML(file)
8
+ Broutes.logger.info {"Loaded #{file} into #{doc.to_s.slice(0, 10)}"}
9
+
10
+ i = 0
11
+ doc.css('trkpt').each do |node|
12
+ p = route.add_point(lat: node['lat'].to_f, lon: node['lon'].to_f, elevation: point_elevation(node), time: point_time(node))
13
+ i += 1
14
+ end
15
+ Broutes.logger.info {"Loaded #{i} data points"}
16
+ end
17
+
18
+ def point_elevation(node)
19
+ if elevation_node = node.at_css('ele')
20
+ elevation_node.inner_text.to_f
21
+ end
22
+ end
23
+
24
+ def point_time(node)
25
+ if time_node = node.at_css('time')
26
+ DateTime.parse(time_node.inner_text).to_time
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,96 @@
1
+ require 'nokogiri'
2
+
3
+ module Broutes::Formats
4
+ class Tcx
5
+ def load(file, route)
6
+ doc = Nokogiri::XML(file)
7
+ Broutes.logger.info {"Loaded #{file} into #{doc.to_s.slice(0, 10)}"}
8
+
9
+ i = 0
10
+ doc.css('Trackpoint').each do |node|
11
+ data = {
12
+ elevation: point_elevation(node),
13
+ time: point_time(node),
14
+ distance: point_distance(node),
15
+ heart_rate: point_heart_rate(node),
16
+ power: point_power(node),
17
+ speed: point_speed(node),
18
+ cadence: point_cadence(node)
19
+ }
20
+ if location = point_location(node)
21
+ data[:lat] = location[0]
22
+ data[:lon] = location[1]
23
+ end
24
+
25
+ p = route.add_point(data)
26
+ i += 1
27
+ end
28
+ Broutes.logger.info {"Loaded #{i} data points"}
29
+
30
+ # Load in summary values if time and distance nil, ie no points
31
+ unless route.total_time
32
+ route.total_time = doc.css('Activities > Activity > Lap > TotalTimeSeconds').reduce(0) { |sum, node|
33
+ sum + node.inner_text.to_i
34
+ }
35
+ end
36
+
37
+ unless route.total_distance
38
+ route.total_distance = doc.css('Activities > Activity > Lap > DistanceMeters').reduce(0) { |sum, node|
39
+ sum + node.inner_text.to_i
40
+ }
41
+ end
42
+
43
+ unless route.started_at
44
+ route.started_at = DateTime.parse(doc.css('Activities > Activity > Lap').first['StartTime']).to_time
45
+ end
46
+ end
47
+
48
+ def point_location(node)
49
+ if position_node = node.at_css('Position')
50
+ [ position_node.at_css('LatitudeDegrees').inner_text.to_f, position_node.at_css('LongitudeDegrees').inner_text.to_f ]
51
+ end
52
+ end
53
+
54
+ def point_distance(node)
55
+ if distance_node = node.at_css('DistanceMeters')
56
+ distance_node.inner_text.to_f
57
+ end
58
+ end
59
+
60
+ def point_elevation(node)
61
+ if elevation_node = node.at_css('AltitudeMeters')
62
+ elevation_node.inner_text.to_f
63
+ end
64
+ end
65
+
66
+ def point_time(node)
67
+ if time_node = node.at_css('Time')
68
+ DateTime.parse(time_node.inner_text).to_time
69
+ end
70
+ end
71
+
72
+ def point_heart_rate(node)
73
+ if hr_node = node.at_css('HeartRateBpm')
74
+ hr_node.inner_text.to_i
75
+ end
76
+ end
77
+
78
+ def point_cadence(node)
79
+ if cadence_node = node.at_css('Cadence')
80
+ cadence_node.inner_text.to_i
81
+ end
82
+ end
83
+
84
+ def point_power(node)
85
+ if power_node = node.at_xpath('.//tpx:Watts', 'tpx' => 'http://www.garmin.com/xmlschemas/ActivityExtension/v2')
86
+ power_node.inner_text.to_i
87
+ end
88
+ end
89
+
90
+ def point_speed(node)
91
+ if speed_node = node.at_xpath('.//tpx:Speed', 'tpx' => 'http://www.garmin.com/xmlschemas/ActivityExtension/v2')
92
+ speed_node.inner_text.to_f
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,9 @@
1
+ module Broutes
2
+ module Formats
3
+ require_relative 'formats/factory'
4
+ require_relative 'formats/gpx_track'
5
+ require_relative 'formats/gpx_route'
6
+ require_relative 'formats/tcx'
7
+ require_relative 'formats/fit_file'
8
+ end
9
+ end
@@ -0,0 +1,56 @@
1
+ require 'time'
2
+
3
+ module Broutes
4
+ class GeoPoint
5
+ attr_accessor :lat, :lon, :elevation, :distance, :heart_rate, :power, :speed, :cadence, :temperature
6
+ attr_reader :time
7
+
8
+ def initialize(args={})
9
+ args.each_pair do |key, value| send("#{key}=", value) if respond_to?("#{key}=") end
10
+ end
11
+
12
+ def self.from_hash(h)
13
+ GeoPoint.new(h)
14
+ end
15
+
16
+ def has_location?
17
+ lat && lon
18
+ end
19
+
20
+ def time=(value)
21
+ if value.is_a?(String)
22
+ @time = DateTime.parse(value).to_time
23
+ else
24
+ @time = value
25
+ end
26
+ end
27
+
28
+ def ==(other)
29
+ lat == other.lat &&
30
+ lon == other.lon &&
31
+ elevation == other.elevation &&
32
+ distance == other.distance &&
33
+ time == other.time &&
34
+ heart_rate == other.heart_rate &&
35
+ power == other.power &&
36
+ speed == other.speed &&
37
+ cadence == other.cadence &&
38
+ temperature == other.temperature
39
+ end
40
+
41
+ def to_hash
42
+ h = {}
43
+ h['lat'] = lat if lat
44
+ h['lon'] = lon if lon
45
+ h['elevation'] = elevation if elevation
46
+ h['distance'] = distance if distance
47
+ h['time'] = time if time
48
+ h['heart_rate'] = heart_rate if heart_rate
49
+ h['power'] = power if power
50
+ h['speed'] = speed if speed
51
+ h['cadence'] = cadence if cadence
52
+ h['temperature'] = temperature if temperature
53
+ h
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,109 @@
1
+ module Broutes
2
+ class GeoRoute
3
+
4
+ attr_reader :start_point, :end_point, :started_at, :ended_at, :total_time
5
+ attr_writer :total_distance, :started_at
6
+ attr_accessor :total_time
7
+
8
+ def points
9
+ get_points.to_enum
10
+ end
11
+
12
+ def initialize(args={})
13
+ args.each_pair do |key, value|
14
+ if key.to_sym == :points
15
+ value.each { |p| add_point(p) }
16
+ else
17
+ send("#{key}=", value) if respond_to?("#{key}=")
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.from_hash(h)
23
+ route = GeoRoute.new h
24
+ end
25
+
26
+ def to_hash
27
+ h = {
28
+ 'total_distance' => total_distance,
29
+ 'total_time' => total_time,
30
+ 'total_ascent' => total_ascent,
31
+ 'total_descent' => total_ascent,
32
+ 'points' => points.collect { |p| p.to_hash }
33
+ }
34
+ h['start_point'] = start_point.to_hash if start_point
35
+ h['end_point'] = end_point.to_hash if end_point
36
+ h['started_at'] = @started_at if @started_at
37
+ h
38
+ end
39
+
40
+ def add_point(args)
41
+ point = GeoPoint.new(args)
42
+ if @start_point
43
+ if point.distance
44
+ @total_distance = point.distance
45
+ else
46
+ if distance = Maths.haversine_distance(@end_point, point)
47
+ @total_distance += distance
48
+ end
49
+ end
50
+
51
+ @total_time = point.time - @start_point.time if point.time
52
+ else
53
+ @start_point = point
54
+ @total_distance = point.distance || 0
55
+ end
56
+
57
+ point.distance = @total_distance
58
+ process_elevation_delta(@end_point, point)
59
+
60
+ @end_point = point
61
+ get_points << point
62
+ end
63
+
64
+ def process_elevation_delta(last_point, new_point)
65
+ if last_point && new_point && last_point.elevation && new_point.elevation
66
+ delta = new_point.elevation - last_point.elevation
67
+ @_total_ascent = self.total_ascent + (delta > 0 ? delta : 0)
68
+ @_total_descent = self.total_descent - (delta < 0 ? delta : 0)
69
+ end
70
+ end
71
+
72
+ def started_at
73
+ return @started_at if @started_at
74
+ @start_point.time if @start_point
75
+ end
76
+
77
+ def ended_at
78
+ @end_point.time if @end_point
79
+ end
80
+
81
+ def total_ascent
82
+ @_total_ascent ||= 0
83
+ end
84
+
85
+ def total_descent
86
+ @_total_descent ||= 0
87
+ end
88
+
89
+ # Public : Total distance measured between points in whole metres
90
+ #
91
+ # Returns Float distance in m
92
+ def total_distance
93
+ @total_distance.round if @total_distance
94
+ end
95
+
96
+ # Public : Measure of how hilly the route is. Measured as total ascent (m) / distance (km)
97
+ #
98
+ # Returns Float measure
99
+ def hilliness
100
+ (total_distance > 0) ? (total_ascent * 1000 / total_distance) : 0
101
+ end
102
+
103
+ private
104
+
105
+ def get_points
106
+ @_points ||= []
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,26 @@
1
+ module Broutes
2
+ module Maths
3
+ class << self
4
+ def haversine_distance(p1, p2)
5
+ return unless p1.has_location? && p2.has_location?
6
+
7
+ dlat = p2.lat - p1.lat
8
+ dlon = p2.lon - p1.lon
9
+
10
+ dlon_rad = dlon * RAD_PER_DEG
11
+ dlat_rad = dlat * RAD_PER_DEG
12
+
13
+ lat1_rad = p1.lat * RAD_PER_DEG
14
+ lon1_rad = p1.lon * RAD_PER_DEG
15
+
16
+ lat2_rad = p2.lat * RAD_PER_DEG
17
+ lon2_rad = p2.lon * RAD_PER_DEG
18
+
19
+ a = (Math.sin(dlat_rad/2))**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * (Math.sin(dlon_rad/2))**2
20
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
21
+
22
+ EARTH_RADIUS * c
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Broutes
2
+ VERSION = "0.1.3".freeze
3
+ end
data/lib/broutes.rb ADDED
@@ -0,0 +1,26 @@
1
+ module Broutes
2
+ require 'logger'
3
+ require_relative 'broutes/geo_route'
4
+ require_relative 'broutes/geo_point'
5
+ require_relative 'broutes/maths'
6
+ require_relative 'broutes/formats'
7
+
8
+ RAD_PER_DEG = 0.017453293 # PI/180
9
+ EARTH_RADIUS = 6371000 #m
10
+
11
+ def self.from_file(file, format)
12
+ raise "unable to interpret format #{format}" unless processor = Formats::Factory.new.get(format)
13
+ Broutes.logger.debug {"found processor #{processor} for #{file}"}
14
+ route = GeoRoute.new
15
+ processor.load(file, route)
16
+ route
17
+ end
18
+
19
+ class << self
20
+ attr_writer :logger
21
+
22
+ def logger
23
+ @logger ||= Logger.new(STDOUT)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Formats::Factory do
4
+ describe "#get" do
5
+
6
+ let(:factory) { Formats::Factory.new }
7
+
8
+ context "when :gpx_track passed" do
9
+ it "returns GpxTrack" do
10
+ factory.get(:gpx_track).should be_an_instance_of(Formats::GpxTrack)
11
+ end
12
+ end
13
+ context "when 'application/vnd.ant.fit' passed" do
14
+ it "returns FitFile" do
15
+ factory.get('application/vnd.ant.fit').should be_an_instance_of(Formats::FitFile)
16
+ end
17
+ end
18
+ context "when :fit passed" do
19
+ it "returns FitFile" do
20
+ factory.get(:fit).should be_an_instance_of(Formats::FitFile)
21
+ end
22
+ end
23
+ context "when unrecognised" do
24
+ it "raises ArgumentError nil" do
25
+ expect { factory.get(random_string) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+ context "tcx filename" do
29
+ it "returns Tcx" do
30
+ factory.get("2012-12-30-12-23.tcx").should be_an_instance_of(Formats::Tcx)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Formats::FitFile do
4
+ describe "#load" do
5
+ before(:all) do
6
+ @file = open_file('sample.fit')
7
+ @target = Formats::FitFile.new
8
+ @route = GeoRoute.new
9
+
10
+ @target.load(@file, @route)
11
+ end
12
+
13
+ it "sets the start point lat" do
14
+ @route.start_point.lat.should eq(52.93066)
15
+ end
16
+ it "sets the start point lon" do
17
+ @route.start_point.lon.should eq(-1.22402)
18
+ end
19
+ it "sets the heart rate" do
20
+ @route.start_point.heart_rate.should eq(88)
21
+ end
22
+ it "sets the power" do
23
+ @route.start_point.power.should eq(96)
24
+ end
25
+ it "extracts the cadence" do
26
+ @route.start_point.cadence.should eq(51)
27
+ end
28
+ it "extracts the speed" do
29
+ @route.start_point.speed.should eq(4.024)
30
+ end
31
+ it "extracts the speed" do
32
+ @route.start_point.temperature.should eq(11.0)
33
+ end
34
+ it "sets the total distance" do
35
+ @route.total_distance.should eq(92068)
36
+ end
37
+ it "sets the total ascent" do
38
+ @route.total_ascent.round.should eq(1176)
39
+ end
40
+ it "sets the total descent" do
41
+ @route.total_descent.round.should eq(1177)
42
+ end
43
+ it "sets the total time" do
44
+ @route.total_time.round.should eq(13675)
45
+ end
46
+ it "sets the started_at" do
47
+ @route.started_at.to_i.should eq(Time.new(2012, 5, 12, 7, 29, 45).to_i)
48
+ end
49
+ it "sets the ended_at" do
50
+ @route.ended_at.to_i.should eq(Time.new(2012, 5, 12, 11, 17, 40).to_i)
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Formats::GpxRoute do
4
+ describe "#load" do
5
+ it "loads it properly"
6
+ end
7
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe Formats::GpxTrack do
4
+ describe "#load" do
5
+ before(:all) do
6
+ @file = open_file('single_lap_gpx_track.gpx')
7
+ @target = Formats::GpxTrack.new
8
+ @route = GeoRoute.new
9
+
10
+ @target.load(@file, @route)
11
+ end
12
+
13
+ it "sets the start point lat" do
14
+ @route.start_point.lat.should eq(52.9552055)
15
+ end
16
+ it "sets the start point lon" do
17
+ @route.start_point.lon.should eq(-1.1558583)
18
+ end
19
+ it "sets the total distance" do
20
+ @route.total_distance.should eq(7088)
21
+ end
22
+ it "sets the total ascent" do
23
+ @route.total_ascent.round.should eq(34)
24
+ end
25
+ it "sets the total descent" do
26
+ @route.total_descent.round.should eq(37)
27
+ end
28
+ it "sets the total time" do
29
+ @route.total_time.round.should eq(1231)
30
+ end
31
+ it "sets the started_at" do
32
+ @route.started_at.to_i.should eq(Time.new(2011, 5, 19, 17, 57, 21).to_i)
33
+ end
34
+ it "sets the ended_at" do
35
+ @route.ended_at.to_i.should eq(Time.new(2011, 5, 19, 18, 17, 52).to_i)
36
+ end
37
+ end
38
+ describe "when file doesn't have elevation" do
39
+ before(:all) do
40
+ @file = open_file('single_lap_gpx_track_no_elevation.gpx')
41
+ @target = Formats::GpxTrack.new
42
+ @route = GeoRoute.new
43
+
44
+ @target.load(@file, @route)
45
+ end
46
+
47
+ it "sets the start point lat" do
48
+ @route.start_point.lat.should eq(52.926467718938)
49
+ end
50
+ it "sets the start point lon" do
51
+ @route.start_point.lon.should eq(-1.216092432889)
52
+ end
53
+ it "sets the total distance" do
54
+ @route.total_distance.should eq(123955)
55
+ end
56
+ it "sets the total ascent" do
57
+ @route.total_ascent.should eq(0)
58
+ end
59
+ it "sets the total descent" do
60
+ @route.total_descent.should eq(0)
61
+ end
62
+ end
63
+ end