quickroute 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|