quickroute 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # QuickRoute JPEG Parser for Ruby
2
+
3
+ Parses the GPS data embedded to JPEG files output by [QuickRoute](http://www.matstroeng.se/quickroute/en/).
4
+
5
+ © Jarkko Laine 2011. Licensed under the [WTFPL](http://en.wikipedia.org/wiki/WTFPL).
6
+
7
+ ## Installation
8
+
9
+ gem install quickroute
10
+
11
+
12
+ **Note!** Only works in Ruby >1.9.2
13
+
14
+ ## Usage
15
+
16
+ ```ruby
17
+ @filename = '2010-silja-rastit.jpg'
18
+ qp = QuickrouteJpegParser.new(@filename, true) # true => calculates
19
+ times and distances.
20
+ qp.sessions.first.route.elapsed_time / 60
21
+ # => 43.766666666666666
22
+ qp.sessions.first.route.segments.first.waypoints.size
23
+ # => 992
24
+ qp.sessions.first.route.segments.first.waypoints.map{|wp| [wp.position.longitude, wp.position.latitude]}
25
+ # => [[22.78103777777778, 60.29233194444444],
26
+ # ...
27
+ # [22.77652638888889, 60.293416388888886],
28
+ # [22.77655638888889, 60.29344361111111]]
29
+ ```
@@ -0,0 +1,12 @@
1
+ module DateTimeParser
2
+ def read_date_time(data)
3
+ val = BinData::Uint64le.read(data)
4
+ LOGGER.debug "val is #{val.inspect}"
5
+ val -= 9223372036854775808 if val >= 9223372036854775808
6
+ val -= 4611686018427387904 if val >= 4611686018427387904
7
+ LOGGER.debug "after tweaks val is #{val.inspect}"
8
+ val = (val - 621355968000000000) / 10000000.0
9
+ LOGGER.debug "time was #{Time.at(val)}"
10
+ val
11
+ end
12
+ end
data/lib/handle.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'matrix'
2
+
3
+ class Handle
4
+ attr_reader :transformation_matrix,
5
+ :parameterized_location,
6
+ :pixel_location,
7
+ :type
8
+
9
+ include BinData
10
+ def initialize(data)
11
+ @transformation_matrix = Matrix.build(3,3) do
12
+ DoubleLe.read(data)
13
+ end
14
+ LOGGER.debug "matrix is #{@transformation_matrix.inspect}"
15
+ @parameterized_location = ParameterizedLocation.new(
16
+ BinData::Uint32le.read(data),
17
+ DoubleLe.read(data)
18
+ )
19
+ LOGGER.debug "parameterized location is #{@parameterized_location.inspect}"
20
+
21
+ # pixel location
22
+ @pixel_location = Point.new(
23
+ DoubleLe.read(data),
24
+ DoubleLe.read(data)
25
+ )
26
+ LOGGER.debug "pixel_location is #{@pixel_location.inspect}"
27
+
28
+ @type = BinData::Int16le.read(data)
29
+ LOGGER.debug "handle type is #{@type}"
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ module JpegReader
2
+ def self.fetch_data_from(filename)
3
+ data = ""
4
+
5
+ File.open(filename, "r") do |f|
6
+ if f.read(2) == "\xff\xd8".to_b
7
+ while !f.eof?
8
+ break if f.read(1) != "\xff".to_b
9
+
10
+ if f.read(1) == "\xe0".to_b # APP0
11
+ quickroute_segment = false
12
+ length = BinData::Uint16be.read(f)
13
+
14
+ if length >= 12
15
+ if f.read(10) == "QuickRoute".to_b
16
+ data << f.read(length - 12)
17
+ quickroute_segment = true
18
+ else
19
+ f.seek(length - 12, ::IO::SEEK_CUR)
20
+ end
21
+ else
22
+ f.seek(length - 2, ::IO::SEEK_CUR)
23
+ end
24
+
25
+ break if !quickroute_segment && !data.empty?
26
+ else
27
+ break
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ data
34
+ end
35
+ end
data/lib/lap.rb ADDED
@@ -0,0 +1,25 @@
1
+ class Lap
2
+ TYPES = {
3
+ :start => 0,
4
+ :lap => 1,
5
+ :stop => 2
6
+ }
7
+
8
+ attr_reader :time, :type
9
+ attr_accessor :position, :distance, :straight_line_distance
10
+
11
+ include DateTimeParser
12
+
13
+ def initialize(data = nil)
14
+ read_data(data) if data
15
+ end
16
+
17
+ def read_data(data)
18
+ @time = read_date_time(data)
19
+ @type = BinData::Uint8be.read(data)
20
+ end
21
+
22
+ def is_of_type?(*types)
23
+ types.any?{|t| type == TYPES[t]}
24
+ end
25
+ end
data/lib/long_lat.rb ADDED
@@ -0,0 +1,53 @@
1
+ class LongLat
2
+ # earth radius in meters
3
+ RHO = 6378200
4
+
5
+ attr_accessor :longitude, :latitude
6
+
7
+ def initialize(longitude, latitude)
8
+ @longitude, @latitude = longitude, latitude
9
+ end
10
+
11
+ def self.from_data(data)
12
+ new(
13
+ BinData::Int32le.read(data) / 3600000.0,
14
+ BinData::Int32le.read(data) / 3600000.0
15
+ )
16
+ end
17
+
18
+ def distance_to(other)
19
+ distance_point_to_point(point_matrix, other.point_matrix)
20
+ end
21
+
22
+ def point_matrix
23
+ Matrix[[RHO * sin_phi * cos_theta],
24
+ [RHO * sin_phi * sin_theta],
25
+ [RHO * cos_phi]]
26
+ end
27
+
28
+ private
29
+
30
+ def sin_phi
31
+ Math::sin(0.5 * Math::PI + latitude / 180 * Math::PI)
32
+ end
33
+
34
+ def cos_phi
35
+ Math::cos(0.5 * Math::PI + latitude / 180 * Math::PI)
36
+ end
37
+
38
+ def sin_theta
39
+ Math::sin(longitude / 180 * Math::PI)
40
+ end
41
+
42
+ def cos_theta
43
+ Math::cos(longitude / 180 * Math::PI)
44
+ end
45
+
46
+ def distance_point_to_point(p0, p1)
47
+ sum = 0
48
+ p0.each_with_index do |el, row, col|
49
+ sum += ((p1[row, col] - el)**2)
50
+ end
51
+ Math.sqrt(sum)
52
+ end
53
+ end
@@ -0,0 +1,8 @@
1
+ class ParameterizedLocation
2
+ attr_reader :segment_index, :value
3
+
4
+ def initialize(segment_index, value)
5
+ @segment_index = segment_index
6
+ @value = value
7
+ end
8
+ end
data/lib/parse_test.rb ADDED
@@ -0,0 +1,4 @@
1
+ require_relative 'quickroute'
2
+ @filename = "../2010-ankkurirastit.jpg"
3
+ qp = QuickrouteJpegParser.new(@filename, false)
4
+ LOGGER.debug qp.inspect
data/lib/person.rb ADDED
@@ -0,0 +1,16 @@
1
+ class Person
2
+ attr_reader :name, :club, :id
3
+
4
+ def initialize(data)
5
+ length = BinData::Uint16le.read(data)
6
+ LOGGER.debug "person length is #{length}"
7
+ @name = BinData::String.new(:length => length).read(data)
8
+ LOGGER.debug "person name is #{@name}"
9
+ length = BinData::Uint16le.read(data)
10
+ LOGGER.debug "club length is #{length}"
11
+ @club = BinData::String.new(:length => length).read(data)
12
+
13
+ @id = BinData::Uint32be.read(data)
14
+ LOGGER.debug "id is #{@id}"
15
+ end
16
+ end
data/lib/point.rb ADDED
@@ -0,0 +1,7 @@
1
+ class Point
2
+ attr_reader :x, :y
3
+
4
+ def initialize(x, y)
5
+ @x, @y = x, y
6
+ end
7
+ end
data/lib/quickroute.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'bindata'
3
+ require "logger"
4
+ require 'binary_search/pure'
5
+
6
+ require_relative 'string_extensions'
7
+ require_relative 'tag_data_extractor'
8
+ require_relative 'date_time_parser'
9
+ require_relative 'jpeg_reader'
10
+
11
+ require_relative 'handle'
12
+ require_relative 'point'
13
+ require_relative 'parameterized_location'
14
+ require_relative 'lap'
15
+ require_relative 'person'
16
+ require_relative 'route'
17
+ require_relative 'route_segment'
18
+ require_relative 'session'
19
+ require_relative 'session_info'
20
+ require_relative 'waypoint'
21
+
22
+ require_relative 'long_lat'
23
+ require_relative 'rectangle'
24
+ require_relative 'quickroute_jpeg_parser'
25
+
26
+ LOGGER = Logger.new(STDOUT)
27
+ LOGGER.level = Logger::WARN
28
+
29
+ #@filename = "../2010-ankkurirastit.jpg"
30
+ #@f = File.open(@filename, 'r')
@@ -0,0 +1,108 @@
1
+ TAGS ={
2
+ 1 => :version,
3
+ 2 => :map_corner_positions,
4
+ 3 => :image_corner_positions,
5
+ 4 => :map_location_and_size_in_pixels,
6
+ 5 => :sessions,
7
+ 6 => :session,
8
+ 7 => :route,
9
+ 8 => :handles,
10
+ 9 => :projection_origin,
11
+ 10 => :laps,
12
+ 11 => :session_info
13
+ }
14
+
15
+ class QuickrouteJpegParser
16
+ include BinData
17
+ attr_reader :sessions, :map_corner_positions, :image_corner_positions,
18
+ :map_location_and_size_in_pixels, :version
19
+
20
+ def initialize(filename, calculate)
21
+ @map_corner_positions = {}
22
+ @image_corner_positions = {}
23
+
24
+ start_time = Time.now
25
+
26
+ data = fetch_data_from(filename)
27
+
28
+ if !data.empty?
29
+ process_data(data)
30
+ calculate_data if calculate
31
+ end
32
+
33
+ end_time = Time.now
34
+ @execution_time = end_time - start_time
35
+ end
36
+
37
+ private
38
+
39
+ def calculate_data
40
+ sessions.each{|s| s.calculate}
41
+ end
42
+
43
+ def fetch_data_from(filename)
44
+ JpegReader.fetch_data_from(filename)
45
+ end
46
+
47
+ def process_data(data)
48
+ LOGGER.debug "Starting to process data"
49
+ data = StringIO.new(data)
50
+
51
+ while !data.eof?
52
+ tag = TagDataExtractor.read(data)
53
+ LOGGER.debug "tag: #{tag.inspect}, tag length: #{tag.data_length.inspect}"
54
+ read_tag(tag, data)
55
+ end
56
+ end
57
+
58
+ def read_tag(tag, data)
59
+ send("set_#{TAGS[tag.tag]}", data)
60
+ end
61
+
62
+ def set_version(data)
63
+ LOGGER.debug "Reading version number"
64
+ @version = version_number(data)
65
+ LOGGER.debug "version is: #{@version}"
66
+ end
67
+
68
+ def set_map_corner_positions(data)
69
+ LOGGER.debug "Reading map corner positions:"
70
+ @map_corner_positions = corner_positions(data)
71
+ LOGGER.debug "#{@map_corner_positions.inspect}"
72
+ end
73
+
74
+ def set_image_corner_positions(data)
75
+ LOGGER.debug "Reading image corner number"
76
+ @image_corner_positions = corner_positions(data)
77
+ LOGGER.debug "#{@image_corner_positions.inspect}"
78
+ end
79
+
80
+ def set_map_location_and_size_in_pixels(data)
81
+ LOGGER.debug "Reading map location and size"
82
+ @map_location_and_size_in_pixels = Rectangle.read(data)
83
+ LOGGER.debug "#{@map_location_and_size_in_pixels.inspect}"
84
+ end
85
+
86
+ def set_sessions(data)
87
+ @sessions = Session.read_sessions(data)
88
+ end
89
+
90
+ def corner_positions(data)
91
+ {:sw => read_long_lat(data),
92
+ :nw => read_long_lat(data),
93
+ :ne => read_long_lat(data),
94
+ :se => read_long_lat(data)}
95
+ end
96
+
97
+ def version_number(data)
98
+ [BinData::Uint8be.read(data),
99
+ BinData::Uint8be.read(data),
100
+ BinData::Uint8be.read(data),
101
+ BinData::Uint8be.read(data)].join(".")
102
+ end
103
+
104
+ def read_long_lat(data)
105
+ LongLat.from_data(data)
106
+ end
107
+
108
+ end
data/lib/rectangle.rb ADDED
@@ -0,0 +1,7 @@
1
+ class Rectangle < BinData::Record
2
+ endian :little
3
+ uint16 :x
4
+ uint16 :y
5
+ uint16 :width
6
+ uint16 :height
7
+ end
data/lib/route.rb ADDED
@@ -0,0 +1,100 @@
1
+ class Route
2
+ WAYPOINT_ATTRIBUTES = {
3
+ :position => 1,
4
+ :time => 2,
5
+ :heart_rate => 4,
6
+ :altitude => 8
7
+ }
8
+
9
+ attr_reader :attributes, :extra_waypoints_attributes_length,
10
+ :segments, :distance, :elapsed_time
11
+
12
+ def self.from_data(data)
13
+ new.read_data(data)
14
+ end
15
+
16
+ def initialize
17
+ LOGGER.debug "initializing new route"
18
+ @segments = []
19
+ @distance = 0
20
+ @elapsed_time = 0
21
+ end
22
+
23
+ def read_data(data)
24
+ @attributes = BinData::Uint16le.read(data)
25
+ @extra_waypoints_attributes_length = BinData::Uint16be.read(data)
26
+ read_segments_from(data)
27
+ self
28
+ end
29
+
30
+ def has_attribute?(attribute)
31
+ 0 != (attributes & WAYPOINT_ATTRIBUTES[attribute])
32
+ end
33
+
34
+ def calculate_parameters
35
+ segments.each{|s| s.calculate_waypoints}
36
+ end
37
+
38
+ def parameterized_location_from_time(time)
39
+ return unless segment = segment_for_time(time)
40
+ segment.parameterized_location_from_time(time)
41
+ end
42
+
43
+ def position_from_parameterized_location(location)
44
+ return unless location
45
+ w0, w1, t = waypoints_and_parameter_from_parameterized_location(location)
46
+
47
+ LongLat.new(
48
+ w0.position.longitude + t * (w1.position.longitude - w0.position.longitude),
49
+ w0.position.latitude + t * (w1.position.latitude - w0.position.latitude)
50
+ )
51
+ end
52
+
53
+ def distance_from_parameterized_location(location)
54
+ return unless location
55
+ w0, w1, t = waypoints_and_parameter_from_parameterized_location(location)
56
+ w0.distance + t * (w1.distance - w0.distance)
57
+ end
58
+
59
+ def waypoints_and_parameter_from_parameterized_location(location)
60
+ return unless location && segment = segments[location.segment_index]
61
+
62
+ waypoints = segment.waypoints
63
+
64
+ value = location.value.to_i
65
+
66
+ if value >= waypoints.size - 1
67
+ value = waypoints.size - 2
68
+ end
69
+
70
+ t = location.value - value
71
+
72
+ waypoints.size < 2 ?
73
+ [waypoints[0], waypoints[0], 0] :
74
+ [waypoints[value], waypoints[value + 1], t]
75
+ end
76
+
77
+ def add_distance(dist)
78
+ @distance += dist
79
+ end
80
+
81
+ def add_elapsed_time(time)
82
+ @elapsed_time += time
83
+ end
84
+
85
+ private
86
+
87
+ def segment_for_time(time)
88
+ segments.find{|s| s.has_time?(time) }
89
+ end
90
+
91
+ def read_segments_from(data)
92
+ segment_count = BinData::Uint32le.read(data)
93
+ LOGGER.debug "reading #{segment_count} segments"
94
+ segment_count.times do |i|
95
+ segment = RouteSegment.new(self, data)
96
+
97
+ @segments << segment
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,77 @@
1
+ class RouteSegment
2
+ EPSILON = 0.001
3
+
4
+ attr_accessor :route, :last_time
5
+ attr_reader :waypoints
6
+
7
+ def initialize(route, data = nil)
8
+ LOGGER.debug "Initializing route segment"
9
+ @route = route
10
+
11
+ @waypoints = []
12
+ read_waypoints(data) if data
13
+ end
14
+
15
+ def read_waypoints(data)
16
+ waypoint_count = BinData::Uint32le.read(data)
17
+ LOGGER.debug "reading #{waypoint_count} waypoints"
18
+ waypoint_count.times do |j|
19
+ waypoint = Waypoint.new(self, data)
20
+ add_waypoint(waypoint)
21
+ @last_time = waypoint.time
22
+ end
23
+ end
24
+
25
+ def add_waypoint(*points)
26
+ points.each do |point|
27
+ waypoints << point
28
+ end
29
+ end
30
+
31
+ def calculate_waypoints
32
+ segment_distance = 0
33
+ waypoints.each_with_index do |wp, idx|
34
+ segment_distance += (idx == 0 ? 0 : wp.distance_to(waypoints[idx - 1]))
35
+ wp.distance = route.distance + segment_distance
36
+
37
+ wp.elapsed_time = route.elapsed_time + wp.time - waypoints.first.time
38
+ end
39
+ @route.add_distance(segment_distance)
40
+ @route.add_elapsed_time(waypoints.last.time - waypoints.first.time)
41
+ end
42
+
43
+ def index
44
+ @route.segments.index(self)
45
+ end
46
+
47
+ def parameterized_location_from_time(time)
48
+ lower, upper = 0, waypoints.size - 1
49
+
50
+ # Binary search to find the closest waypoint
51
+ while lower <= upper
52
+ idx = lower + (upper - lower) / 2
53
+ currtime = waypoints[idx].time
54
+ if (time - currtime).abs < EPSILON
55
+ return ParameterizedLocation.new(index, idx)
56
+ end
57
+
58
+ if time < currtime
59
+ upper = idx - 1
60
+ else
61
+ lower = idx + 1
62
+ end
63
+ end
64
+
65
+ t0 = waypoints[upper].time
66
+ t1 = waypoints[lower].time
67
+ if t1 == t0
68
+ return ParameterizedLocation.new(index, upper)
69
+ end
70
+
71
+ ParameterizedLocation.new(index, upper + (time - t0) / (t1 - t0))
72
+ end
73
+
74
+ def has_time?(time)
75
+ ((waypoints.first.time - EPSILON)..(waypoints.last.time + EPSILON)).include?(time)
76
+ end
77
+ end
data/lib/session.rb ADDED
@@ -0,0 +1,113 @@
1
+ class Session
2
+ include BinData
3
+ attr_accessor :route
4
+ attr_reader :laps, :handles,
5
+ :projection_origin, :session_info,
6
+ :straight_line_distance
7
+
8
+ def self.read_sessions(data)
9
+ sessions = []
10
+ session_count = BinData::Uint32le.read(data)
11
+ LOGGER.debug "reading #{session_count} sessions"
12
+
13
+ session_count.times do |i|
14
+ tag = TagDataExtractor.read(data)
15
+
16
+ LOGGER.debug "in session #{i}, tag is #{tag} and is #{tag.data_length} bytes long"
17
+
18
+ if TAGS[tag.tag] == :session
19
+ sessions << read_session(data, tag.data_length)
20
+ end
21
+ end
22
+ sessions
23
+ end
24
+
25
+ def self.read_session(data, tag_data_length)
26
+ new.parse_data(data, tag_data_length)
27
+ end
28
+
29
+ def initialize
30
+ @laps = []
31
+ @handles = []
32
+ @straight_line_distance = 0
33
+ LOGGER.debug "Reading new session"
34
+ end
35
+
36
+ def calculate
37
+ LOGGER.debug("starting to calculate shit for #{self.inspect}")
38
+ route.calculate_parameters
39
+ calculate_laps
40
+ end
41
+
42
+ def calculate_laps
43
+ last_distance, last_lap = 0, nil
44
+
45
+ @laps.each do |lap|
46
+ pl = route.parameterized_location_from_time(lap.time)
47
+ lap.position = route.position_from_parameterized_location(pl)
48
+
49
+ distance = route.distance_from_parameterized_location(pl)
50
+ if lap.is_of_type?(:lap, :stop)
51
+ lap.distance = distance - last_distance
52
+
53
+ if last_lap
54
+ lap.straight_line_distance = lap.position.distance_to(last_lap.position)
55
+ else
56
+ lap.straight_line_distance = 0
57
+ end
58
+ @straight_line_distance += lap.straight_line_distance
59
+ end
60
+ last_distance = distance
61
+ last_lap = lap
62
+ end
63
+
64
+ LOGGER.debug("Laps after calculation: #{@laps.inspect}")
65
+ end
66
+
67
+ def parse_data(data, tag_data_length)
68
+ start_pos = data.pos
69
+ while data.pos < (start_pos + tag_data_length)
70
+ tag = TagDataExtractor.read(data)
71
+
72
+ LOGGER.debug "tag is #{tag}, #{tag.data_length} bytes"
73
+
74
+ send("set_#{TAGS[tag.tag]}", data)
75
+ end
76
+ self
77
+ end
78
+
79
+ private
80
+
81
+ def set_route(data)
82
+ LOGGER.debug "reading route"
83
+ @route = Route.new.read_data(data)
84
+ end
85
+
86
+ def set_handles(data)
87
+ LOGGER.debug "reading handles"
88
+ handle_count = Uint32le.read(data)
89
+ LOGGER.debug "reading #{handle_count} handles"
90
+ handle_count.times do |i|
91
+ @handles << Handle.new(data)
92
+ end
93
+ end
94
+
95
+ def set_projection_origin(data)
96
+ LOGGER.debug "reading project origin"
97
+ @projection_origin = LongLat.from_data(data)
98
+ end
99
+
100
+ def set_laps(data)
101
+ LOGGER.debug "reading laps"
102
+ lap_count = Uint32le.read(data)
103
+ LOGGER.debug "reading #{lap_count} laps"
104
+ lap_count.times do
105
+ @laps << Lap.new(data)
106
+ end
107
+ end
108
+
109
+ def set_session_info(data)
110
+ LOGGER.debug "reading session info"
111
+ @session_info = SessionInfo.new(data)
112
+ end
113
+ end
@@ -0,0 +1,11 @@
1
+ class SessionInfo
2
+ attr_reader :person
3
+
4
+ def initialize(data)
5
+ @person = Person.new(data)
6
+
7
+ length = BinData::Uint16le.read(data)
8
+ LOGGER.debug "description length is #{length}"
9
+ @description = BinData::String.new(:length => length).read(data)
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def to_b
3
+ BinData::String.new(self).to_binary_s
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class TagDataExtractor < BinData::Record
2
+ endian :little
3
+ uint8be :tag
4
+ uint32 :data_length
5
+ end
data/lib/waypoint.rb ADDED
@@ -0,0 +1,58 @@
1
+ class Waypoint
2
+ include BinData
3
+ include DateTimeParser
4
+
5
+ attr_reader :segment, :time, :heart_rate, :altitude
6
+ attr_accessor :distance, :position, :elapsed_time
7
+
8
+ def initialize(segment, data = nil)
9
+ @segment = segment
10
+ @distance = 0
11
+ read_data(data) if data
12
+ end
13
+
14
+ def read_data(data)
15
+ if route.has_attribute?(:position)
16
+ LOGGER.debug "route did have the position attribute"
17
+ @position = LongLat.from_data(data)
18
+ LOGGER.debug "waypoint position: #{@position.inspect}"
19
+ end
20
+
21
+ if route.has_attribute?(:time)
22
+ LOGGER.debug "route did have the time attribute"
23
+ time_type = BinData::Uint8le.read(data)
24
+ if time_type == 0
25
+ time = read_date_time(data)
26
+ else
27
+ time = last_time + BinData::Uint16le.read(data) / 1000
28
+ end
29
+ LOGGER.debug "time was #{Time.at(time)}"
30
+ @time = time
31
+ end
32
+
33
+ if route.has_attribute?(:heart_rate)
34
+ LOGGER.debug "route did have the heart rate attribute"
35
+ @heartrate = Uint8be.read(data)
36
+ end
37
+
38
+ if route.has_attribute?(:altitude)
39
+ LOGGER.debug "route did have the altitude attribute"
40
+ @altitude = Uint16le.read(data)
41
+ LOGGER.debug "altitude was #{@altitude}"
42
+ end
43
+
44
+ data.seek(route.extra_waypoints_attributes_length, ::IO::SEEK_CUR)
45
+ end
46
+
47
+ def last_time
48
+ segment.last_time
49
+ end
50
+
51
+ def route
52
+ segment.route
53
+ end
54
+
55
+ def distance_to(other)
56
+ position.distance_to(other.position)
57
+ end
58
+ end
@@ -0,0 +1,108 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe "Parsing existing jpg file without calculation" do
4
+ before(:all) do
5
+ @filename = File.join(File.expand_path(File.dirname(__FILE__)),
6
+ '../fixtures/2010-Ankkurirastit.jpg')
7
+
8
+ @qp = QuickrouteJpegParser.new(File.join(@filename), false)
9
+ end
10
+
11
+ it "should have correct version" do
12
+ @qp.version.should == "1.0.0.0"
13
+ end
14
+
15
+ it "should have correct size in pixels" do
16
+ @qp.map_location_and_size_in_pixels.width.should == 1365
17
+ @qp.map_location_and_size_in_pixels.height.should == 1556
18
+ end
19
+
20
+ it "should have one session" do
21
+ @qp.sessions.size.should equal(1)
22
+ end
23
+
24
+ describe "session" do
25
+ before do
26
+ @it = @qp.sessions.first
27
+ end
28
+
29
+ it "should have correct session info" do
30
+ @it.session_info.person.name.should == "kari ja maria"
31
+ end
32
+
33
+ it "should have 17 laps" do
34
+ @it.laps.size.should equal(17)
35
+ end
36
+
37
+ describe "lap" do
38
+ it "should have correct time" do
39
+ Time.at(@it.laps.first.time).to_s.should == "2010-04-18 11:48:03 +0300"
40
+ end
41
+ end
42
+
43
+ it "should have 39 handles" do
44
+ @it.handles.size.should equal(39)
45
+ end
46
+
47
+ it "should have correct projection origin" do
48
+ @it.projection_origin.latitude.should == 60.33081694444444
49
+ @it.projection_origin.longitude.should == 22.894455
50
+ end
51
+
52
+ describe "route" do
53
+ it "should have one segment" do
54
+ @it.route.segments.size.should equal(1)
55
+ end
56
+
57
+ describe "segment" do
58
+ it "should have 1001 waypoints" do
59
+ @it.route.segments.first.waypoints.size.should equal(1001)
60
+ end
61
+
62
+ describe "first waypoint" do
63
+ before do
64
+ @wp = @it.route.segments.first.waypoints.first
65
+ end
66
+
67
+ it "should have correct time set" do
68
+ Time.at(@wp.time).to_s.should == "2010-04-18 11:48:03 +0300"
69
+ end
70
+
71
+ it "should have correct position" do
72
+ @wp.position.latitude.should == 60.34066361111111
73
+ @wp.position.longitude.should == 22.904983333333334
74
+ end
75
+
76
+ it "should have correct altitude" do
77
+ @wp.altitude.should == 62
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "Parsing existing jpg file with calculation" do
86
+ before(:all) do
87
+ @filename = File.join(File.expand_path(File.dirname(__FILE__)),
88
+ '../fixtures/2010-Ankkurirastit.jpg')
89
+
90
+ @qp = QuickrouteJpegParser.new(File.join(@filename), true)
91
+ end
92
+
93
+ describe "session" do
94
+ it "should have correct straight line distance" do
95
+ @qp.sessions.first.straight_line_distance.round.should == 6750
96
+ end
97
+ end
98
+
99
+ describe "route" do
100
+ it "should have correct distance" do
101
+ @qp.sessions.first.route.distance.round.should == 7566
102
+ end
103
+
104
+ it "should have correct elapsed time" do
105
+ @qp.sessions.first.route.elapsed_time.should == 2706.0
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Lap do
4
+ before(:each) do
5
+ @it = Lap.new
6
+ end
7
+
8
+ describe "setting position" do
9
+ it "should set the position ivar and be readable" do
10
+ long_lat = LongLat.new(0,0)
11
+ @it.position = long_lat
12
+
13
+ @it.position.should == long_lat
14
+ end
15
+ end
16
+
17
+ describe "setting distance" do
18
+ it "should work" do
19
+ @it.distance = 69
20
+ @it.distance.should == 69
21
+ end
22
+ end
23
+
24
+ describe "setting straight line distance" do
25
+ it "should work" do
26
+ @it.straight_line_distance = 69
27
+ @it.straight_line_distance.should == 69
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe QuickrouteJpegParser do
4
+ describe '#new' do
5
+ describe 'with calculate == false' do
6
+ it "should not calculate values" do
7
+ #TagDataExtractor.stub!(:extract_tag_data).and_return([5, 16])
8
+ JpegReader.stub!(:fetch_data_from).and_return("")
9
+ @session = Session.new
10
+ Session.stub!(:read_sessions).and_return([@session])
11
+ @session.should_not_receive(:calculate)
12
+ @parser = QuickrouteJpegParser.new("filename", false)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,50 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe RouteSegment do
4
+ before(:each) do
5
+ @route = Route.new
6
+ @it = RouteSegment.new(@route)
7
+ @wp1 = Waypoint.new(@it)
8
+ @wp2 = Waypoint.new(@it)
9
+ @wp3 = Waypoint.new(@it)
10
+ end
11
+
12
+ describe "#add_waypoints" do
13
+ it "should add waypoints to the segment" do
14
+ @it.add_waypoint(@wp1, @wp2, @wp3)
15
+ @it.waypoints.should == [@wp1, @wp2, @wp3]
16
+ end
17
+ end
18
+
19
+ describe "#calculate_waypoints" do
20
+ before(:each) do
21
+ @it.add_waypoint(@wp1, @wp2, @wp3)
22
+ @wp2.should_receive(:distance_to).with(@wp1).and_return(10)
23
+ @wp3.should_receive(:distance_to).with(@wp2).and_return(20)
24
+
25
+ t1 = Time.now.to_i
26
+ @wp1.should_receive(:time).at_least(:once).and_return(t1)
27
+ @wp2.should_receive(:time).at_least(:once).and_return(t1 + 60)
28
+ @wp3.should_receive(:time).at_least(:once).and_return(t1 + 180)
29
+
30
+ @it.calculate_waypoints
31
+ end
32
+
33
+ it "should calculate the total distance for each waypoint" do
34
+ @wp1.distance.should == 0
35
+ @wp2.distance.should == 10
36
+ @wp3.distance.should == 30
37
+ end
38
+
39
+ it "should calculate the total elapsed time for each waypoint" do
40
+ @wp1.elapsed_time.should == 0
41
+ @wp2.elapsed_time.should == 60
42
+ @wp3.elapsed_time.should == 180
43
+ end
44
+
45
+ it "should update route's distance and time in the end" do
46
+ @route.distance.should == 30
47
+ @route.elapsed_time == 180
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Route do
4
+ before(:each) do
5
+ @it = Route.new
6
+ end
7
+
8
+ describe "#calculate_parameters" do
9
+ before(:each) do
10
+ @segment = stub
11
+ @it.stub(:segments).and_return([@segment])
12
+ end
13
+
14
+ it "should calculate total distance for each segment" do
15
+ @segment.should_receive(:calculate_waypoints)
16
+ @it.calculate_parameters
17
+ end
18
+ end
19
+
20
+ describe "#add_distance" do
21
+ it "should add given distance" do
22
+ @it.distance.should == 0
23
+ @it.add_distance(180)
24
+ @it.distance.should == 180
25
+ end
26
+ end
27
+
28
+ describe "#add_elapsed_time" do
29
+ it "should add given elapsed time" do
30
+ @it.elapsed_time.should == 0
31
+ @it.add_elapsed_time(180)
32
+ @it.elapsed_time.should == 180
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Session do
4
+ before(:each) do
5
+ @it = Session.new
6
+ @it.route = @route = Route.new
7
+ end
8
+
9
+ describe "calculate" do
10
+ it "should call route.calculate" do
11
+ @route.should_receive(:calculate_parameters)
12
+ @it.calculate
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Waypoint do
4
+ before(:each) do
5
+ @route = Route.new
6
+ @segment = stub(:segment, :route => @route)
7
+ @position = LongLat.new(0,0)
8
+ LongLat.stub(:from_data).and_return(@position)
9
+ @it = Waypoint.new(@segment)
10
+ end
11
+
12
+ it "should have a zero distance" do
13
+ @it.distance.should == 0
14
+ end
15
+
16
+ it "should be able to set elapsed time" do
17
+ @it.elapsed_time = 180
18
+ @it.elapsed_time.should == 180
19
+ end
20
+
21
+ describe "#distance_to" do
22
+ it "should delegate to position" do
23
+ @other = Waypoint.new(@segment)
24
+ @op = LongLat.new(0,1)
25
+ @other.position = @op
26
+ @it.position = @position
27
+ @position.should_receive(:distance_to).with(@op).and_return(50)
28
+ @it.distance_to(@other).should == 50
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1 @@
1
+ require_relative '../lib/quickroute'
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quickroute
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jarkko Laine
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70239720740460 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.7.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70239720740460
25
+ - !ruby/object:Gem::Dependency
26
+ name: bindata
27
+ requirement: &70239720739980 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 1.4.3
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70239720739980
36
+ - !ruby/object:Gem::Dependency
37
+ name: binary_search
38
+ requirement: &70239720739520 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 0.2.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70239720739520
47
+ description: Library for parsing the GPS data embedded to JPG files by QuickRoute
48
+ (http://www.matstroeng.se/quickroute/en/).
49
+ email: jarkko@jlaine.net
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/date_time_parser.rb
55
+ - lib/handle.rb
56
+ - lib/jpeg_reader.rb
57
+ - lib/lap.rb
58
+ - lib/long_lat.rb
59
+ - lib/parameterized_location.rb
60
+ - lib/parse_test.rb
61
+ - lib/person.rb
62
+ - lib/point.rb
63
+ - lib/quickroute.rb
64
+ - lib/quickroute_jpeg_parser.rb
65
+ - lib/rectangle.rb
66
+ - lib/route.rb
67
+ - lib/route_segment.rb
68
+ - lib/session.rb
69
+ - lib/session_info.rb
70
+ - lib/string_extensions.rb
71
+ - lib/tag_data_extractor.rb
72
+ - lib/waypoint.rb
73
+ - spec/fixtures/2010-Ankkurirastit.jpg
74
+ - spec/integration/parsing_spec.rb
75
+ - spec/models/lap_spec.rb
76
+ - spec/models/quickroute_jpeg_parser_spec.rb
77
+ - spec/models/route_segment_spec.rb
78
+ - spec/models/route_spec.rb
79
+ - spec/models/session_spec.rb
80
+ - spec/models/waypoint_spec.rb
81
+ - spec/spec_helper.rb
82
+ - README.md
83
+ homepage: https://github.com/jarkko/quickroute-ruby
84
+ licenses: []
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: 1.9.2
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 1.8.13
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Parser library for QuickRoute JPG files with embedded route data
107
+ test_files: []