quickroute 0.1.0
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.
- data/README.md +29 -0
- data/lib/date_time_parser.rb +12 -0
- data/lib/handle.rb +31 -0
- data/lib/jpeg_reader.rb +35 -0
- data/lib/lap.rb +25 -0
- data/lib/long_lat.rb +53 -0
- data/lib/parameterized_location.rb +8 -0
- data/lib/parse_test.rb +4 -0
- data/lib/person.rb +16 -0
- data/lib/point.rb +7 -0
- data/lib/quickroute.rb +30 -0
- data/lib/quickroute_jpeg_parser.rb +108 -0
- data/lib/rectangle.rb +7 -0
- data/lib/route.rb +100 -0
- data/lib/route_segment.rb +77 -0
- data/lib/session.rb +113 -0
- data/lib/session_info.rb +11 -0
- data/lib/string_extensions.rb +5 -0
- data/lib/tag_data_extractor.rb +5 -0
- data/lib/waypoint.rb +58 -0
- data/spec/fixtures/2010-Ankkurirastit.jpg +0 -0
- data/spec/integration/parsing_spec.rb +108 -0
- data/spec/models/lap_spec.rb +30 -0
- data/spec/models/quickroute_jpeg_parser_spec.rb +16 -0
- data/spec/models/route_segment_spec.rb +50 -0
- data/spec/models/route_spec.rb +35 -0
- data/spec/models/session_spec.rb +15 -0
- data/spec/models/waypoint_spec.rb +32 -0
- data/spec/spec_helper.rb +1 -0
- metadata +107 -0
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
|
data/lib/jpeg_reader.rb
ADDED
@@ -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
|
data/lib/parse_test.rb
ADDED
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
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
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
|
data/lib/session_info.rb
ADDED
@@ -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
|
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
|
Binary file
|
@@ -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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|