pangdudu-mamba 0.1
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.rdoc +26 -0
- data/lib/b1trackerdata/b1_data_parser.rb +230 -0
- data/lib/b1trackerdata/openglviewer.rb +278 -0
- data/lib/graph.rb +194 -0
- data/lib/mamba.rb +324 -0
- data/tests/generate_testdata.rb +150 -0
- metadata +128 -0
data/README.rdoc
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
= Mamba
|
|
2
|
+
|
|
3
|
+
Hi girls and boys!
|
|
4
|
+
|
|
5
|
+
This is a Qt4 ruby tool for analysis of ART tracker data time series generated
|
|
6
|
+
by the ART motion capturing system.
|
|
7
|
+
|
|
8
|
+
== Installation
|
|
9
|
+
|
|
10
|
+
sudo gem install pangdudu-mamba --source=http://gems.github.com
|
|
11
|
+
|
|
12
|
+
== Usage
|
|
13
|
+
|
|
14
|
+
ruby mamba.rb
|
|
15
|
+
|
|
16
|
+
== Config
|
|
17
|
+
|
|
18
|
+
Is still hardcoded in the source files. :)
|
|
19
|
+
|
|
20
|
+
== Rule the universe!
|
|
21
|
+
|
|
22
|
+
Oh mighty gods of ruby, please make the GIL go away!
|
|
23
|
+
|
|
24
|
+
== License
|
|
25
|
+
|
|
26
|
+
GPL -> http://www.gnu.org/licenses/gpl.txt
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
This file e.g. the TrackerDataParser fetches the tracker data from the
|
|
3
|
+
database, and parses them according to the "SFB673TPB1 Annotation Manual".
|
|
4
|
+
=end
|
|
5
|
+
|
|
6
|
+
require "rubygems"
|
|
7
|
+
require "narray"
|
|
8
|
+
require "sequel"
|
|
9
|
+
require "rofl"
|
|
10
|
+
|
|
11
|
+
#connect to the database
|
|
12
|
+
DB = Sequel.connect(:adapter=>'mysql', :host=>'localhost', :database=>'database', :user=>'user', :password=>'password')
|
|
13
|
+
|
|
14
|
+
class TrackerDataParser
|
|
15
|
+
|
|
16
|
+
attr_accessor :data
|
|
17
|
+
|
|
18
|
+
def initialize trial,test=false
|
|
19
|
+
@trial = trial
|
|
20
|
+
tablename = "Tracker#{trial}"
|
|
21
|
+
tablename = "TrackerTest" if test
|
|
22
|
+
dlog "Trying to connect to table: #{tablename}"
|
|
23
|
+
#get our rows
|
|
24
|
+
@rows = DB[tablename.to_sym]
|
|
25
|
+
@highpass = 0.25 #for later filtering
|
|
26
|
+
dlog "Table has #{@rows.count} rows."
|
|
27
|
+
#processed data will end up here
|
|
28
|
+
@data = {:left => {}, :right => {}}
|
|
29
|
+
#do we know that one marker has been worn wrong?
|
|
30
|
+
@data[:left_twist] = false
|
|
31
|
+
@data[:right_twist] = false
|
|
32
|
+
#get and parse the tracker data
|
|
33
|
+
operate
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#main controller function
|
|
37
|
+
def operate
|
|
38
|
+
@data[:reference] = (parsetrackerdata @rows.filter(:identifier => "Neck")).sort
|
|
39
|
+
dlog "Parsed neckdata count is #{@data[:reference].length}"
|
|
40
|
+
left_raw = (parsetrackerdata @rows.filter(:identifier => "Left_Hand")).sort
|
|
41
|
+
dlog "Left hand quality is #{(Float(left_raw.length)/Float(@data[:reference].length))*100}%"
|
|
42
|
+
right_raw = (parsetrackerdata @rows.filter(:identifier => "Right_Hand")).sort
|
|
43
|
+
dlog "Right hand quality is #{(Float(right_raw.length)/Float(@data[:reference].length))*100}%"
|
|
44
|
+
|
|
45
|
+
#sometimes people are wearing the tracker wrong, need to fix this
|
|
46
|
+
fix_marker_twist
|
|
47
|
+
|
|
48
|
+
#now parse the tracker data for both hands
|
|
49
|
+
left_raw.each do |timestamp,matrix|
|
|
50
|
+
d = getannotationdata timestamp,matrix,@data[:left_polarity]
|
|
51
|
+
@data[:left][timestamp] = d unless d.nil?
|
|
52
|
+
end
|
|
53
|
+
right_raw.each do |timestamp,matrix|
|
|
54
|
+
d = getannotationdata timestamp,matrix,@data[:right_polarity]
|
|
55
|
+
@data[:right][timestamp] = d unless d.nil?
|
|
56
|
+
end
|
|
57
|
+
#Debug and info
|
|
58
|
+
ilog "Statistics for trial #{@trial}:"
|
|
59
|
+
ilog " Left hand - parsed: #{@data[:left].length}"
|
|
60
|
+
ilog " Right hand - parsed: #{@data[:right].length}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#method parsing tracking data from the db and loading it into an hash(organized by timestamp)
|
|
64
|
+
def parsetrackerdata trackerdata
|
|
65
|
+
parseddata = {}
|
|
66
|
+
trackerdata.each do |d|
|
|
67
|
+
m = NMatrix.float(4,4)
|
|
68
|
+
#that's how the coordinates would look if the matrix was transposed (now the bottom is 0,0,0,1)
|
|
69
|
+
m[0,0],m[0,1],m[0,2],m[0,3] = d[:m00], d[:m01], d[:m02], d[:m03]
|
|
70
|
+
m[1,0],m[1,1],m[1,2],m[1,3] = d[:m10], d[:m11], d[:m12], d[:m13]
|
|
71
|
+
m[2,0],m[2,1],m[2,2],m[2,3] = d[:m20], d[:m21], d[:m22], d[:m23]
|
|
72
|
+
m[3,0],m[3,1],m[3,2],m[3,3] = d[:m30], d[:m31], d[:m32], d[:m33]
|
|
73
|
+
m = m.transpose #matrix data in the db are transposed
|
|
74
|
+
parseddata[Float(d[:timestamp])] = m
|
|
75
|
+
end
|
|
76
|
+
return parseddata
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def getannotationdata timestamp,matrix,polarity
|
|
80
|
+
annotationdata = {}
|
|
81
|
+
reference = @data[:reference][timestamp][1]
|
|
82
|
+
relative = reference.inverse * matrix * polarity
|
|
83
|
+
annotationdata[:timestamp] = timestamp
|
|
84
|
+
annotationdata[:reference] = reference
|
|
85
|
+
annotationdata[:data] = matrix
|
|
86
|
+
annotationdata[:relative] = relative
|
|
87
|
+
annotationdata[:boh] = getbackofhanddirection relative
|
|
88
|
+
annotationdata[:pd] = getpalmdirection relative
|
|
89
|
+
annotationdata[:wp] = getwristposition relative
|
|
90
|
+
annotationdata[:we] = getwristextend relative
|
|
91
|
+
return annotationdata
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#function computing palm direction
|
|
95
|
+
def getpalmdirection data
|
|
96
|
+
#negative z seems to be the palm direction
|
|
97
|
+
#you should check the data in the visualization to be sure
|
|
98
|
+
x,y,z = (-1)*data[2,0],(-1)*data[2,1],(-1)*data[2,2]
|
|
99
|
+
result = []
|
|
100
|
+
result << "PAB" if z < -@highpass
|
|
101
|
+
result << "PDN" if y < -@highpass
|
|
102
|
+
result << "PTB" if z > @highpass
|
|
103
|
+
result << "PTL" if x < -@highpass
|
|
104
|
+
result << "PTR" if x > @highpass
|
|
105
|
+
result << "PUP" if y > @highpass
|
|
106
|
+
return result.join("/")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
#function computing back of hand direction
|
|
110
|
+
def getbackofhanddirection data
|
|
111
|
+
#ok, it looks like the markers y axis is the boh direction
|
|
112
|
+
#you should check the data in the visualization to be sure
|
|
113
|
+
x,y,z = data[1,0],data[1,1],data[1,2]
|
|
114
|
+
result = []
|
|
115
|
+
result << "BAB" if z < -@highpass
|
|
116
|
+
result << "BDN" if y < -@highpass
|
|
117
|
+
result << "BTB" if z > @highpass
|
|
118
|
+
result << "BTL" if x < -@highpass
|
|
119
|
+
result << "BTR" if x > @highpass
|
|
120
|
+
result << "BUP" if y > @highpass
|
|
121
|
+
return result.join("/")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#function computing wrist extend
|
|
125
|
+
def getwristextend data
|
|
126
|
+
result = "aight!"
|
|
127
|
+
x,y,z = data[3,0],data[3,1],data[3,2]
|
|
128
|
+
#puts "wp: x '#{x}', y '#{y}', z '#{z}'"
|
|
129
|
+
zf = 100 #normalizing factor (100 = SH standard human)
|
|
130
|
+
xf = 100 #normalizing factor (100 = SH standard human)
|
|
131
|
+
x = x*xf
|
|
132
|
+
z = z*zf
|
|
133
|
+
r = Math.sqrt(x*x + z*z)
|
|
134
|
+
#ok, need to put real values in here (look at the annotationmanual)
|
|
135
|
+
result = "D-GTO" if r > 80 #greater than length of outstretched arm in front away
|
|
136
|
+
result = "D-KO" if r <= 80 #between knee and length of outstretched arm in front away
|
|
137
|
+
result = "D-EK" if r < 65 #between elbow and knee
|
|
138
|
+
result = "D-CE" if r < 45 #between body and elbows length away
|
|
139
|
+
result = "D-C" if r < 20 #in contact with body
|
|
140
|
+
#puts "extend is '#{result}'"
|
|
141
|
+
return result
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#function computing wrist position
|
|
145
|
+
def getwristposition data
|
|
146
|
+
result = "aight!"
|
|
147
|
+
#negative z seems to be palm direction
|
|
148
|
+
#you should check the data in the visualization to be sure
|
|
149
|
+
x,y,z = data[3,0],data[3,1],data[3,2]
|
|
150
|
+
yf = 100
|
|
151
|
+
xf = 100
|
|
152
|
+
x = x*xf
|
|
153
|
+
y = y*yf
|
|
154
|
+
#puts "wp: x '#{x}', y '#{y}', z '#{z}'"
|
|
155
|
+
#will not be rock solid, so we go from outwards->inwards
|
|
156
|
+
#extreme periphery
|
|
157
|
+
eperiphery = "EP-"
|
|
158
|
+
eperiphery += "UP" if(y > 30 && x.abs <= 19)
|
|
159
|
+
eperiphery += "UL" if(y > 17 && x < -19)
|
|
160
|
+
eperiphery += "UR" if(y > 17 && x > 19)
|
|
161
|
+
eperiphery += "LT" if(y <= 17 && y > -17 && x < -31.5)
|
|
162
|
+
eperiphery += "RT" if(y <= 17 && y > -17 && x > 31.5)
|
|
163
|
+
eperiphery += "LW" if(y < -30 && x.abs <= 19)
|
|
164
|
+
eperiphery += "LL" if(y < -17 && x < -19)
|
|
165
|
+
eperiphery += "LR" if(y < -17 && x > 19)
|
|
166
|
+
result = eperiphery unless eperiphery.eql?("EP-")
|
|
167
|
+
#periphery
|
|
168
|
+
if (x.abs <= 31.5 && y.abs <= 30)
|
|
169
|
+
periphery = "P-"
|
|
170
|
+
periphery += "UP" if(y > 17 && x.abs <= 13)
|
|
171
|
+
periphery += "UL" if(y > 9 && x < -13)
|
|
172
|
+
periphery += "UR" if(y > 9 && x > 13)
|
|
173
|
+
periphery += "LT" if(y <= 9 && y > -10 && x < -21)
|
|
174
|
+
periphery += "RT" if(y <= 9 && y > -10 && x > 21)
|
|
175
|
+
periphery += "LW" if(y <= -10 && y > -30 && x.abs <= 13)
|
|
176
|
+
periphery += "LL" if(y <= -10 && y > -30 && x < -13)
|
|
177
|
+
periphery += "LR" if(y <= -10 && y > -30 && x > 13)
|
|
178
|
+
result = periphery unless periphery.eql?("P-")
|
|
179
|
+
end
|
|
180
|
+
#center
|
|
181
|
+
if (x.abs <= 21 && y.abs <= 17)
|
|
182
|
+
center = "C-"
|
|
183
|
+
center += "UP" if(y > 9 && x.abs <= 10)
|
|
184
|
+
center += "UL" if(y > 4.5 && x < -10)
|
|
185
|
+
center += "UR" if(y > 4.5 && x > 10)
|
|
186
|
+
center += "LT" if(y <= 4.5 && y > -7 && x < -13)
|
|
187
|
+
center += "RT" if(y <= 4.5 && y > -7 && x > 13)
|
|
188
|
+
center += "LW" if(y <= -7 && y > -17 && x.abs <= 10)
|
|
189
|
+
center += "LL" if(y <= -7 && y > -17 && x < -10)
|
|
190
|
+
center += "LR" if(y <= -7 && y > -17 && x > 10)
|
|
191
|
+
result = center unless center.eql?("C-")
|
|
192
|
+
end
|
|
193
|
+
#center-center
|
|
194
|
+
if (x.abs <= 13 && y.abs <= 9)
|
|
195
|
+
result = "CC"
|
|
196
|
+
end
|
|
197
|
+
#puts "result #{result}"
|
|
198
|
+
return result
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
#method to fix the direction of the marker, if it has been worn wrong
|
|
202
|
+
def fix_marker_twist
|
|
203
|
+
#left hand
|
|
204
|
+
@data[:left_polarity] = NMatrix.float(4,4).unit
|
|
205
|
+
if @data[:left_twist]
|
|
206
|
+
p = NMatrix.float(4,4).unit
|
|
207
|
+
#if y direction of tracker is WRONG we need to rotate around z axis by 180 deg
|
|
208
|
+
p[0,0], p[0,1], p[1,0], p[1,1] = Math.cos(Math::PI),-Math.sin(Math::PI),Math.sin(Math::PI),Math.cos(Math::PI)
|
|
209
|
+
ilog "Will multiply left hand with this polarity matrix: #{p.inspect}"
|
|
210
|
+
@data[:left_polarity] = p
|
|
211
|
+
end
|
|
212
|
+
#and the right hand
|
|
213
|
+
@data[:right_polarity] = NMatrix.float(4,4).unit
|
|
214
|
+
if @data[:right_twist]
|
|
215
|
+
p = NMatrix.float(4,4).unit
|
|
216
|
+
#if y direction of tracker is WRONG we need to rotate around z axis by 180 deg
|
|
217
|
+
p[0,0], p[0,1], p[1,0], p[1,1] = Math.cos(Math::PI),-Math.sin(Math::PI),Math.sin(Math::PI),Math.cos(Math::PI)
|
|
218
|
+
ilog "Will multiply right hand with this polarity matrix: #{p.inspect}"
|
|
219
|
+
@data[:right_polarity] = p
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
#for testing
|
|
225
|
+
if false
|
|
226
|
+
tdp = TrackerDataParser.new "V06",true #will select table TrackerTest
|
|
227
|
+
require "openglviewer"
|
|
228
|
+
ogv = OpenGlViewer.new tdp.data
|
|
229
|
+
ogv.run
|
|
230
|
+
end
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'opengl'
|
|
3
|
+
require 'rational'
|
|
4
|
+
require 'narray'
|
|
5
|
+
require 'rofl'
|
|
6
|
+
#include Gl,Glu,Glut
|
|
7
|
+
|
|
8
|
+
class OpenGlViewer
|
|
9
|
+
|
|
10
|
+
attr_accessor :currenttimestamp,:timestamps,:pause
|
|
11
|
+
|
|
12
|
+
STDOUT.sync = TRUE
|
|
13
|
+
POS = [0, 0, 0, 0.0]
|
|
14
|
+
|
|
15
|
+
def initialize data
|
|
16
|
+
@data = data
|
|
17
|
+
@base = NMatrix.float(4,4).unit
|
|
18
|
+
@matrices = {}
|
|
19
|
+
@matrices[:base] = @base
|
|
20
|
+
@currenttimestamp = 0.0
|
|
21
|
+
@current = 0
|
|
22
|
+
@timestamps = {}
|
|
23
|
+
@pause = true
|
|
24
|
+
@forward = true
|
|
25
|
+
@leftboh = "none"
|
|
26
|
+
@rightboh = "none"
|
|
27
|
+
filltimestamps
|
|
28
|
+
glShadeModel(GL::SMOOTH)
|
|
29
|
+
glNormal(0.0, 0.0, 1.0)
|
|
30
|
+
glLightfv(GL::LIGHT0, GL::POSITION, POS)
|
|
31
|
+
glEnable(GL::CULL_FACE)
|
|
32
|
+
glEnable(GL::LIGHTING)
|
|
33
|
+
glEnable(GL::LIGHT0)
|
|
34
|
+
glEnable(GL::DEPTH_TEST)
|
|
35
|
+
glEnable(GL::LINE_SMOOTH)
|
|
36
|
+
glEnable(GL::POINT_SMOOTH)
|
|
37
|
+
glClearColor(0.0, 0.0, 0.0, 0.0)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def filltimestamps
|
|
41
|
+
i = 0
|
|
42
|
+
@data[:reference].each do |timestamp,value|
|
|
43
|
+
@timestamps[i] = timestamp
|
|
44
|
+
i += 1
|
|
45
|
+
end
|
|
46
|
+
ilog "Found '#{@timestamps.count}'"
|
|
47
|
+
ilog "Will now try to display '#{@data[:left].count}' left and '#{@data[:right].count}' right hand values"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fillmatrices
|
|
51
|
+
unless @data[:left][@currenttimestamp].nil?
|
|
52
|
+
#@matrices[:reference] = @data[:left][@currenttimestamp][:reference]
|
|
53
|
+
#@matrices[:left_hand_data] = @data[:left][@currenttimestamp][:data]
|
|
54
|
+
@matrices[:left_hand_relative] = @data[:left][@currenttimestamp][:relative]
|
|
55
|
+
@leftboh = @data[:left][@currenttimestamp][:boh]
|
|
56
|
+
end
|
|
57
|
+
unless @data[:right][@currenttimestamp].nil?
|
|
58
|
+
#@matrices[:reference] = @data[:right][@currenttimestamp][:reference]
|
|
59
|
+
#@matrices[:right_hand_data] = @data[:right][@currenttimestamp][:data]
|
|
60
|
+
@matrices[:right_hand_relative] = @data[:right][@currenttimestamp][:relative]
|
|
61
|
+
@rightboh = @data[:right][@currenttimestamp][:boh]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def setcurrenttimestamp
|
|
66
|
+
unless @pause
|
|
67
|
+
if @forward
|
|
68
|
+
@current += 1
|
|
69
|
+
@currenttimestamp = @timestamps[@current]
|
|
70
|
+
end
|
|
71
|
+
if !@forward
|
|
72
|
+
@current -= 1 unless (@current.eql? 0)
|
|
73
|
+
@currenttimestamp = @timestamps[@current]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# gl display method
|
|
79
|
+
def display
|
|
80
|
+
fillmatrices
|
|
81
|
+
setcurrenttimestamp
|
|
82
|
+
fps_control(60)
|
|
83
|
+
glClear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT)
|
|
84
|
+
glColor(0.0, 0.0, 0.0)
|
|
85
|
+
glPushMatrix()
|
|
86
|
+
glRotate(@view_rotx, 1.0, 0.0, 0.0)
|
|
87
|
+
glRotate(@view_roty, 0.0, 1.0, 0.0)
|
|
88
|
+
glRotate(@view_rotz, 0.0, 0.0, 1.0)
|
|
89
|
+
@matrices.each { |k,v| draw_object k,v }
|
|
90
|
+
displayinfo
|
|
91
|
+
glPopMatrix()
|
|
92
|
+
glutSwapBuffers()
|
|
93
|
+
@frames = 0 if not defined? @frames
|
|
94
|
+
@t0 = 0 if not defined? @t0
|
|
95
|
+
@frames += 1
|
|
96
|
+
t = GLUT.Get(GLUT::ELAPSED_TIME)
|
|
97
|
+
if t - @t0 >= 5000
|
|
98
|
+
seconds = (t - @t0) / 1000.0
|
|
99
|
+
fps = @frames / seconds
|
|
100
|
+
@t0, @frames = t, 0
|
|
101
|
+
exit if defined? @autoexit and t >= 999.0 * @autoexit
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def drawOneLine x1,y1,z1,x2,y2,z2
|
|
106
|
+
glBegin(GL_LINES)
|
|
107
|
+
glVertex(x1,y1,z1)
|
|
108
|
+
glVertex(x2,y2,z2)
|
|
109
|
+
glEnd()
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def drawLineWithText text,scale,x,y,z
|
|
113
|
+
scale2 = scale*scale
|
|
114
|
+
glPushMatrix()
|
|
115
|
+
drawOneLine(0,0,0,scale*x,scale*y,scale*z)
|
|
116
|
+
glTranslate((scale*x),(scale*y),(scale*z))
|
|
117
|
+
glScale(1.0/(scale2),1.0/(scale2),1.0/(scale2))
|
|
118
|
+
text.each_byte { |x| glutStrokeCharacter(GLUT_STROKE_ROMAN, x) }
|
|
119
|
+
glScale(scale2,scale2,scale2)
|
|
120
|
+
glTranslate(-(scale*x),-(scale*y),-(scale*z))
|
|
121
|
+
glPopMatrix()
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# draw an imd and its childs
|
|
125
|
+
def draw_object key,matrix
|
|
126
|
+
glPushMatrix()
|
|
127
|
+
scale = 10
|
|
128
|
+
translation_scale = 2
|
|
129
|
+
glTranslate(matrix[3,0]*scale*translation_scale,matrix[3,1]*scale*translation_scale,matrix[3,2]*scale*translation_scale)
|
|
130
|
+
glColor(1,0,0)
|
|
131
|
+
drawLineWithText "#{key} x",scale,matrix[0,0],matrix[0,1],matrix[0,2]
|
|
132
|
+
glColor(0,1,0)
|
|
133
|
+
drawLineWithText "#{key} y",scale,matrix[1,0],matrix[1,1],matrix[1,2]
|
|
134
|
+
glColor(0,0,1)
|
|
135
|
+
drawLineWithText "#{key} z",scale,matrix[2,0],matrix[2,1],matrix[2,2]
|
|
136
|
+
glTranslate(-matrix[3,0]*scale*translation_scale,-matrix[3,1]*scale*translation_scale,-matrix[3,2]*scale*translation_scale)
|
|
137
|
+
glPopMatrix()
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def displayinfo
|
|
141
|
+
info = "Information:"
|
|
142
|
+
info += " timestamp: #{(@timestamps[@current]).to_s}"
|
|
143
|
+
info += " PAUSED" if @pause
|
|
144
|
+
info += " FORWARD" if (!@pause && @forward)
|
|
145
|
+
info += " REVERSE" if (!@pause && !@forward)
|
|
146
|
+
info += " BOH left:#{@leftboh} right:#{@rightboh}"
|
|
147
|
+
scale = 10
|
|
148
|
+
scale2 = scale*scale
|
|
149
|
+
glPushMatrix()
|
|
150
|
+
glTranslate((scale),(scale),(scale))
|
|
151
|
+
glScale(1.0/(scale2),1.0/(scale2),1.0/(scale2))
|
|
152
|
+
info.each_byte { |x| glutStrokeCharacter(GLUT_STROKE_ROMAN, x) }
|
|
153
|
+
glScale(scale2,scale2,scale2)
|
|
154
|
+
glTranslate(-(scale),-(scale),-(scale))
|
|
155
|
+
glPopMatrix()
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def fps_control(fps)
|
|
159
|
+
t = GLUT.Get(GLUT::ELAPSED_TIME)
|
|
160
|
+
@t0_idle = t if !defined? @t0_idle
|
|
161
|
+
time_to_sleep = (1000./fps) - (t - @t0_idle)
|
|
162
|
+
sleep(time_to_sleep.to_f/1000) if time_to_sleep > 0
|
|
163
|
+
@t0_idle = t
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# gets called when window is resized etc.
|
|
167
|
+
def reshape width, height
|
|
168
|
+
h = height.to_f / width.to_f
|
|
169
|
+
glViewport(0, 0, width, height)
|
|
170
|
+
glMatrixMode(GL::PROJECTION)
|
|
171
|
+
glLoadIdentity()
|
|
172
|
+
glFrustum(-1.0, 1.0, -h, h, 1.0, 1000.0)
|
|
173
|
+
glMatrixMode(GL::MODELVIEW)
|
|
174
|
+
setViewpoint
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def idle
|
|
178
|
+
GLUT.PostRedisplay()
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def setViewpoint
|
|
182
|
+
glMatrixMode(GL_MODELVIEW)
|
|
183
|
+
glLoadIdentity()
|
|
184
|
+
gluLookAt(@eyex, 0, @znear, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
|
|
185
|
+
glutPostRedisplay()
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Change view angle
|
|
189
|
+
def special (k, x, y)
|
|
190
|
+
case k
|
|
191
|
+
when GLUT::KEY_UP
|
|
192
|
+
@view_rotx += 5.0
|
|
193
|
+
when GLUT::KEY_DOWN
|
|
194
|
+
@view_rotx -= 5.0
|
|
195
|
+
when GLUT::KEY_LEFT
|
|
196
|
+
@view_roty += 5.0
|
|
197
|
+
when GLUT::KEY_RIGHT
|
|
198
|
+
@view_roty -= 5.0
|
|
199
|
+
end
|
|
200
|
+
GLUT.PostRedisplay()
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# mouse ;)
|
|
204
|
+
def mouse button, state, x, y
|
|
205
|
+
changed = false
|
|
206
|
+
case button
|
|
207
|
+
when 3 then @eyex -= 5; changed = true;
|
|
208
|
+
when 4 then @eyex += 5; changed = true
|
|
209
|
+
end
|
|
210
|
+
setViewpoint if changed
|
|
211
|
+
@mouse = state
|
|
212
|
+
@x0, @y0 = x, y
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# motion control
|
|
216
|
+
def motion x, y
|
|
217
|
+
if @mouse == GLUT::DOWN then
|
|
218
|
+
@view_rotz += @x0 - x
|
|
219
|
+
@view_roty += @y0 - y
|
|
220
|
+
setViewpoint
|
|
221
|
+
end
|
|
222
|
+
@x0, @y0 = x, y
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
#keyboard control handling
|
|
226
|
+
def keyboard(key, x, y)
|
|
227
|
+
case (key)
|
|
228
|
+
when ?s
|
|
229
|
+
@znear = @znear - 1
|
|
230
|
+
when ?w
|
|
231
|
+
@znear = @znear + 1
|
|
232
|
+
when ?a
|
|
233
|
+
@eyex = @eyex - 1
|
|
234
|
+
when ?d
|
|
235
|
+
@eyex = @eyex + 1
|
|
236
|
+
when ?f
|
|
237
|
+
@forward = true
|
|
238
|
+
when ?r
|
|
239
|
+
@forward = false
|
|
240
|
+
when ?p
|
|
241
|
+
togglepause
|
|
242
|
+
when 27 # Escape
|
|
243
|
+
exit
|
|
244
|
+
end
|
|
245
|
+
setViewpoint
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def togglepause
|
|
249
|
+
@pause = !@pause
|
|
250
|
+
puts "Toggle pause now '#{@pause}'"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def visible(vis)
|
|
254
|
+
GLUT.IdleFunc((vis == GLUT::VISIBLE ? method(:idle).to_proc : nil))
|
|
255
|
+
end
|
|
256
|
+
#the loop
|
|
257
|
+
def run
|
|
258
|
+
@znear = 30
|
|
259
|
+
@eyex = 0
|
|
260
|
+
@view_rotx, @view_roty, @view_rotz = 0, 0, 0
|
|
261
|
+
glutInit
|
|
262
|
+
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
|
|
263
|
+
glutInitWindowSize(640, 480)
|
|
264
|
+
glutInitWindowPosition(0, 0)
|
|
265
|
+
glutCreateWindow($0)
|
|
266
|
+
glutDisplayFunc(method(:display).to_proc)
|
|
267
|
+
glutReshapeFunc(method(:reshape).to_proc)
|
|
268
|
+
glutSpecialFunc(method(:special).to_proc)
|
|
269
|
+
glutKeyboardFunc(method(:keyboard).to_proc)
|
|
270
|
+
glutMouseFunc(method(:mouse).to_proc)
|
|
271
|
+
glutMotionFunc(method(:motion).to_proc)
|
|
272
|
+
glutVisibilityFunc(method(:visible).to_proc)
|
|
273
|
+
glShadeModel(GL::SMOOTH)
|
|
274
|
+
glNormal(0.0, 0.0, 1.0)
|
|
275
|
+
glClearColor(0.0, 0.0, 0.0, 0.0)
|
|
276
|
+
glutMainLoop()
|
|
277
|
+
end
|
|
278
|
+
end
|
data/lib/graph.rb
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
This class is a little B1 specific. It implements the graph we'll later
|
|
3
|
+
visualize.
|
|
4
|
+
=end
|
|
5
|
+
|
|
6
|
+
require "rubygems"
|
|
7
|
+
require "narray"
|
|
8
|
+
require "Qt4"
|
|
9
|
+
require "rofl"
|
|
10
|
+
require "b1trackerdata/b1_data_parser"
|
|
11
|
+
|
|
12
|
+
class Graph
|
|
13
|
+
|
|
14
|
+
attr_accessor :points,:deltas
|
|
15
|
+
|
|
16
|
+
def initialize trial,deltas,hfrac,test=false
|
|
17
|
+
#deltas we will compute
|
|
18
|
+
@deltas = deltas
|
|
19
|
+
ilog "Will compute deltas for #{@deltas.inspect}."
|
|
20
|
+
#in case the deltas are to small/big change this factor
|
|
21
|
+
@delta_factor = 256
|
|
22
|
+
#in case the angles are to small/big change this factor
|
|
23
|
+
@euler_factor = hfrac
|
|
24
|
+
#this factor governs how many pixels the points are away from another
|
|
25
|
+
@x_factor = 4
|
|
26
|
+
#get the data from the database
|
|
27
|
+
parser = TrackerDataParser.new trial,test
|
|
28
|
+
@data = parser.data
|
|
29
|
+
#generate unique array with all occuring timestamps and prepare @values
|
|
30
|
+
@timestamps = build_timestamps
|
|
31
|
+
#fill values (deltas,etc.)
|
|
32
|
+
@previous = {:left => NMatrix.float(4,4), :right => NMatrix.float(4,4)}
|
|
33
|
+
fill_values
|
|
34
|
+
#build the points array (this will be used later on to draw the graph)
|
|
35
|
+
@points = build_points
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#build an array with all occuring timestamps
|
|
39
|
+
def build_timestamps
|
|
40
|
+
@values = {}
|
|
41
|
+
tss = []
|
|
42
|
+
@data[:reference].each { |t,v| tss << t }
|
|
43
|
+
@data[:left].each { |t,v| tss << t }
|
|
44
|
+
@data[:right].each { |t,v| tss << t }
|
|
45
|
+
dlog "tss count: #{tss.length}"
|
|
46
|
+
timestamps = (tss.uniq).sort
|
|
47
|
+
dlog "Unique timestamps: #{timestamps.length}"
|
|
48
|
+
dlog "Unique timestamps first: #{timestamps.first}"
|
|
49
|
+
#now preparing @values
|
|
50
|
+
timestamps.each do |t|
|
|
51
|
+
@values[t] = {:left => {}, :right => {}}
|
|
52
|
+
end
|
|
53
|
+
return timestamps
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#fill values with parsed data
|
|
57
|
+
def fill_values
|
|
58
|
+
[:left,:right].each do |hand|
|
|
59
|
+
dlog "Data for #{hand.inspect} has #{@data[hand].length} entries."
|
|
60
|
+
#it's crucial to do sort here, otherwise the timestamps are mixed up
|
|
61
|
+
@data[hand].sort.each do |t,v|
|
|
62
|
+
wlog "Empty or nil value hash!" if v.nil? or v.empty?
|
|
63
|
+
#we have tracker data
|
|
64
|
+
v[:blackout] = false
|
|
65
|
+
#will only happen the first time
|
|
66
|
+
@previous[hand] = v[:relative] if @previous[hand].abs.sum == 0.0
|
|
67
|
+
#compute euler angles
|
|
68
|
+
v[:euler] = compute_euler_angles(v)
|
|
69
|
+
#compute deltas for plotting
|
|
70
|
+
v[:deltas] = compute_deltas(hand,v)
|
|
71
|
+
#for next delta computation
|
|
72
|
+
@previous[hand] = v[:relative]
|
|
73
|
+
#put values into @values
|
|
74
|
+
@values[t][hand] = v
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
dlog "Processed #{@values.length} values."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#compute euler angles for plotting
|
|
81
|
+
#http://en.wikipedia.org/wiki/Euler_angles
|
|
82
|
+
def compute_euler_angles values
|
|
83
|
+
#we'll put the angles in here
|
|
84
|
+
e = {}
|
|
85
|
+
#get the hand matrix
|
|
86
|
+
m = values[:relative]
|
|
87
|
+
#alpha = atan2(-Z2, Z1)
|
|
88
|
+
e[:alpha] = { :value => Math.atan2(-1.0*m[2,1],m[2,0]) }
|
|
89
|
+
#beta = atan2(Z3, sqrt(Z1^2+Z2^2))
|
|
90
|
+
e[:beta] = { :value => Math.atan2(m[2,2],Math.sqrt(m[2,0]**2+m[2,1]**2)) }
|
|
91
|
+
#gamma = -atan2(Y3,-X3) if Z3 < 0
|
|
92
|
+
e[:gamma] = { :value => -1.0*Math.atan2(m[1,2],-1.0*m[0,2]) } if m[2,2] <= 0.0
|
|
93
|
+
#gamma = atan2(Y3,X3) if Z3 > 0
|
|
94
|
+
e[:gamma] = { :value => Math.atan2(m[1,2],m[0,2]) } if m[2,2] > 0.0
|
|
95
|
+
return e
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#compute deltas for plotting
|
|
99
|
+
def compute_deltas hand,values,t=0
|
|
100
|
+
#we'll put the deltas in here
|
|
101
|
+
ds = {}
|
|
102
|
+
#get the matrix
|
|
103
|
+
m = values[:relative]
|
|
104
|
+
#to reduce computation time we only compute what's needed
|
|
105
|
+
if @deltas.include? :simple
|
|
106
|
+
#compute simple delta over the complete 4x4 matrix
|
|
107
|
+
value = (m-@previous[hand]).abs.sum
|
|
108
|
+
ds[:simple] = { :value => value }
|
|
109
|
+
end
|
|
110
|
+
if @deltas.include? :pos
|
|
111
|
+
#compute simple delta over the position vector
|
|
112
|
+
value = (m[3,0..2]-@previous[hand][3,0..2]).abs.sum
|
|
113
|
+
ds[:pos] = { :value => value }
|
|
114
|
+
end
|
|
115
|
+
if @deltas.include? :rot
|
|
116
|
+
#compute simple delta over the 3x3 rotation matrix
|
|
117
|
+
value = (m[0..2,0..2]-@previous[hand][0..2,0..2]).abs.sum
|
|
118
|
+
ds[:rot] = { :value => value }
|
|
119
|
+
end
|
|
120
|
+
if @deltas.include? :xrot
|
|
121
|
+
#compute simple delta over the x-rotation axis vector
|
|
122
|
+
value = (m[0,0..2]-@previous[hand][0,0..2]).abs.sum
|
|
123
|
+
ds[:xrot] = { :value => value }
|
|
124
|
+
end
|
|
125
|
+
if @deltas.include? :yrot
|
|
126
|
+
#compute simple delta over the y-rotation axis vector
|
|
127
|
+
value = (m[1,0..2]-@previous[hand][1,0..2]).abs.sum
|
|
128
|
+
ds[:yrot] = { :value => value }
|
|
129
|
+
end
|
|
130
|
+
if @deltas.include? :zrot
|
|
131
|
+
#compute simple delta over the z-rotation axis vector
|
|
132
|
+
value = (m[2,0..2]-@previous[hand][2,0..2]).abs.sum
|
|
133
|
+
ds[:zrot] = { :value => value }
|
|
134
|
+
end
|
|
135
|
+
#and return the computed deltas
|
|
136
|
+
return ds
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
#build the points array we'll use later on to draw the graph
|
|
140
|
+
def build_points
|
|
141
|
+
@timestamps.each_index do |i|
|
|
142
|
+
ts = @timestamps[i]
|
|
143
|
+
#do the following for both hands
|
|
144
|
+
[:left,:right].each do |h|
|
|
145
|
+
#if tracker data are missing we substitute
|
|
146
|
+
if @values.has_key? ts
|
|
147
|
+
@values[ts][h] = get_missing_values if @values[ts][h].empty?
|
|
148
|
+
end
|
|
149
|
+
#set index and x position
|
|
150
|
+
@values[ts][h][:i] = i
|
|
151
|
+
x = i*@x_factor
|
|
152
|
+
@values[ts][h][:x] = x
|
|
153
|
+
#now we create a Qt::PointF.new(x,y) for each euler angle
|
|
154
|
+
@values[ts][h][:euler].each do |name,a|
|
|
155
|
+
#create Point, mirror hands on x axis and apply y-scaling
|
|
156
|
+
y = 0
|
|
157
|
+
unless @values[ts][h][:blackout]
|
|
158
|
+
y += @euler_factor-a[:value]*@euler_factor/Math::PI
|
|
159
|
+
y *= -1.0 if h.eql? :left
|
|
160
|
+
end
|
|
161
|
+
#and add the point to the euler values
|
|
162
|
+
@values[ts][h][:euler][name][:point] = Qt::PointF.new(x,y)
|
|
163
|
+
end
|
|
164
|
+
#now we create a Qt::PointF.new(x,y) for every delta type
|
|
165
|
+
@values[ts][h][:deltas].each do |type,delta|
|
|
166
|
+
#create Point, mirror hands on x axis and apply y-scaling
|
|
167
|
+
y = delta[:value]*@delta_factor
|
|
168
|
+
y *= -1.0 if h.eql? :left
|
|
169
|
+
#and add the point to the delta values
|
|
170
|
+
@values[ts][h][:deltas][type][:point] = Qt::PointF.new(x,y)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
dlog "Processed #{@values.length} timestamps."
|
|
175
|
+
points = @values.sort
|
|
176
|
+
dlog "points array has #{points.length} entries."
|
|
177
|
+
return points
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
#in case values are missing for timestamps (no tracker data), we'll supply values
|
|
181
|
+
def get_missing_values
|
|
182
|
+
#we'll put the deltas in here
|
|
183
|
+
missing_deltas = {}
|
|
184
|
+
#put 0.0 to deltas
|
|
185
|
+
@deltas.each { |d| missing_deltas[d] = { :value => 0.0 } }
|
|
186
|
+
#we'll put the euler angles in here
|
|
187
|
+
missing_euler = {}
|
|
188
|
+
#put 0.0 to euler angles
|
|
189
|
+
[:alpha,:beta,:gamma].each { |a| missing_euler[a] = { :value => 0.0 } }
|
|
190
|
+
#and build the values array
|
|
191
|
+
v = { :deltas => missing_deltas, :euler => missing_euler, :blackout => true }
|
|
192
|
+
return v
|
|
193
|
+
end
|
|
194
|
+
end
|
data/lib/mamba.rb
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
require "rubygems"
|
|
2
|
+
require "rofl"
|
|
3
|
+
require "Qt4"
|
|
4
|
+
require "graph"
|
|
5
|
+
#require "unprof"
|
|
6
|
+
|
|
7
|
+
#oky, this will become a visualizer for large data sequences
|
|
8
|
+
|
|
9
|
+
class Gui < Qt::Widget
|
|
10
|
+
|
|
11
|
+
attr_accessor :width,:height
|
|
12
|
+
attr_accessor :x_delta,:mousex,:mousey
|
|
13
|
+
|
|
14
|
+
def initialize(width,height,parent = nil)
|
|
15
|
+
super()
|
|
16
|
+
@width,@height = width,height
|
|
17
|
+
resize(@width,@height)
|
|
18
|
+
@lfw = 16 # legend font width
|
|
19
|
+
@tfw = 15 # timestamp font width
|
|
20
|
+
@pfw = 11 # predicate font width
|
|
21
|
+
#text will not be filled
|
|
22
|
+
#@brush = Qt::NoBrush
|
|
23
|
+
#text will be filled
|
|
24
|
+
@brush = Qt::Brush.new Qt::Color.new 0, 0, 0
|
|
25
|
+
@current_color = 0
|
|
26
|
+
#generate these deltas
|
|
27
|
+
deltas = []
|
|
28
|
+
#display these predicates
|
|
29
|
+
@predicates = [:boh, :pd, :wp, :we]
|
|
30
|
+
#graph with the data we want to visualize
|
|
31
|
+
#@graph = Graph.new "V06",deltas,@height/12
|
|
32
|
+
@graph = Graph.new "",deltas,@height/12,true #will build graph from table 'TrackerTest'
|
|
33
|
+
#max movement speed
|
|
34
|
+
@speedlimit = 20
|
|
35
|
+
#setup the rest
|
|
36
|
+
setup
|
|
37
|
+
#method doing the x scrolling for us
|
|
38
|
+
dlog "Initialized, starting active scroll loop."
|
|
39
|
+
reactive_scroll
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#setup all the stuff we need to display
|
|
43
|
+
def setup
|
|
44
|
+
#legend painter path
|
|
45
|
+
@legend = build_legend_path
|
|
46
|
+
#text painter path
|
|
47
|
+
@text = build_text_path
|
|
48
|
+
#graph painter paths
|
|
49
|
+
@graphs = setup_graph_paths
|
|
50
|
+
#now some gui specific stuff
|
|
51
|
+
setPalette(Qt::Palette.new(Qt::Color.new(250, 250, 250)))
|
|
52
|
+
setAutoFillBackground(true)
|
|
53
|
+
#cursor settings
|
|
54
|
+
@cur1 = self.cursor
|
|
55
|
+
@cur2 = Qt::Cursor.new(Qt::PointingHandCursor)
|
|
56
|
+
self.mouseTracking = true
|
|
57
|
+
#used for scrolling objects to the left and right
|
|
58
|
+
@xdelta,@xgoal,@xspeed = 0.0,0.0,0.0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#build painter path for the legend
|
|
62
|
+
def build_legend_path
|
|
63
|
+
p = Qt::PainterPath.new
|
|
64
|
+
p.moveTo 0,0
|
|
65
|
+
font = Qt::Font.new "Arial", @lfw
|
|
66
|
+
p.addText @lfw,1.5*@lfw, font, "LEFT: euler angles"
|
|
67
|
+
p.addText @lfw,2*@height/12, font, "RIGHT: euler angles"
|
|
68
|
+
p.addText @lfw,5*@height/12, font, "LEFT: data"
|
|
69
|
+
p.addText @lfw,7*@height/12, font, "RIGHT: data"
|
|
70
|
+
p.addText @lfw,10*@height/12-@lfw, font, "LEFT: blackout"
|
|
71
|
+
p.addText @lfw,10*@height/12+@lfw, font, "RIGHT: blackout"
|
|
72
|
+
p.addText @lfw,11*@height/12-@lfw/2, font, "timestamps"
|
|
73
|
+
dlog "Finished legend path setup."
|
|
74
|
+
return p
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#build painter path for the text data
|
|
78
|
+
def build_text_path
|
|
79
|
+
#y to timestamps
|
|
80
|
+
ty = 11*@height/12
|
|
81
|
+
#back to left hand data
|
|
82
|
+
ypl = -5*@height/12-2*@lfw
|
|
83
|
+
#and back to right hand data
|
|
84
|
+
ypr = -3*@height/12-2*@lfw
|
|
85
|
+
tfont = Qt::Font.new "Arial", @tfw
|
|
86
|
+
pfont = Qt::Font.new "Arial", @pfw
|
|
87
|
+
every = 25 #display text every ... frames
|
|
88
|
+
current = 0
|
|
89
|
+
p = Qt::PainterPath.new
|
|
90
|
+
p.moveTo 0,0
|
|
91
|
+
@graph.points.each do |ts,hands|
|
|
92
|
+
if current >= every
|
|
93
|
+
p.addText hands[:left][:x],@lfw, tfont, "#{ts}"
|
|
94
|
+
#display the predicates
|
|
95
|
+
@predicates.each_index do |i|
|
|
96
|
+
p.addText hands[:left][:x], ypl+i*@lfw, pfont, "#{hands[:left][@predicates[i]]}"
|
|
97
|
+
p.addText hands[:right][:x], ypr+i*@lfw, pfont, "#{hands[:right][@predicates[i]]}"
|
|
98
|
+
end
|
|
99
|
+
current = 0
|
|
100
|
+
else
|
|
101
|
+
current += 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
dlog "Finished subtitles path setup."
|
|
105
|
+
return { :y => ty, :path => p }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#setup the graph paths we want to paint
|
|
109
|
+
def setup_graph_paths
|
|
110
|
+
dlog "Starting graph path setup."
|
|
111
|
+
graph_paths = {}
|
|
112
|
+
#build following graphs for both hands
|
|
113
|
+
[:left,:right].each do |hand|
|
|
114
|
+
#build painter paths for blackout graph
|
|
115
|
+
blackout = build_blackout_path(hand)
|
|
116
|
+
graph_paths[blackout[:name]] = blackout
|
|
117
|
+
dlog "Finished blackout path setup. Y: #{blackout[:y]}"
|
|
118
|
+
#build painter paths for euler angles
|
|
119
|
+
[:alpha,:beta,:gamma].each do |angle|
|
|
120
|
+
euler = build_euler_path(hand,angle)
|
|
121
|
+
graph_paths[euler[:name]] = euler
|
|
122
|
+
dlog "Finished #{euler[:name]} path setup. Y: #{euler[:y]}"
|
|
123
|
+
end
|
|
124
|
+
#build painter paths for deltas
|
|
125
|
+
@graph.deltas.each do |d|
|
|
126
|
+
delta = build_delta_path(hand,d)
|
|
127
|
+
graph_paths[delta[:name]] = delta
|
|
128
|
+
dlog "Finished #{delta[:name]} path setup. Y: #{delta[:y]}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
dlog "Finished graph path setup."
|
|
132
|
+
return graph_paths
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#build painter path for blackout graph
|
|
136
|
+
def build_blackout_path hand
|
|
137
|
+
name = "blackout_#{hand}".to_sym
|
|
138
|
+
color = Qt::Color.new(0, 0, 64) if hand.eql? :left
|
|
139
|
+
color = Qt::Color.new(64, 0, 0) if hand.eql? :right
|
|
140
|
+
#y-translation of the graph, we only need y
|
|
141
|
+
ty = 10*@height/12+@lfw/2
|
|
142
|
+
ty -= @lfw if hand.eql? :left
|
|
143
|
+
ty += @lfw if hand.eql? :right
|
|
144
|
+
path = Qt::PainterPath.new
|
|
145
|
+
path.moveTo 0,0
|
|
146
|
+
#now we construct the path
|
|
147
|
+
@graph.points.each do |ts,v|
|
|
148
|
+
unless v[hand][:blackout]
|
|
149
|
+
path.moveTo v[hand][:x],0
|
|
150
|
+
else
|
|
151
|
+
path.lineTo v[hand][:x],0
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
#and return the values
|
|
155
|
+
return { :y => ty, :name => name, :path => path, :color => color, :width => 2.5 }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
#build painter path for euler graph
|
|
159
|
+
def build_euler_path hand,angle
|
|
160
|
+
name = "#{angle}_#{hand}".to_sym
|
|
161
|
+
color = Qt::Color.new(255, 0, 0) if angle.eql? :alpha
|
|
162
|
+
color = Qt::Color.new(0, 255, 0) if angle.eql? :gamma
|
|
163
|
+
color = Qt::Color.new(0, 0, 255) if angle.eql? :beta
|
|
164
|
+
#y-translation of the graph, we only need y
|
|
165
|
+
ty = @height/6
|
|
166
|
+
path = Qt::PainterPath.new
|
|
167
|
+
path.moveTo 0,0
|
|
168
|
+
#now we construct the path
|
|
169
|
+
@graph.points.each do |ts,v|
|
|
170
|
+
path.lineTo v[hand][:euler][angle][:point]
|
|
171
|
+
end
|
|
172
|
+
#and return the values
|
|
173
|
+
return { :y => ty, :name => name, :path => path, :color => color }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
#build painter path for delta graph
|
|
177
|
+
def build_delta_path hand,delta
|
|
178
|
+
name = "#{delta}_#{hand}".to_sym
|
|
179
|
+
color = Qt::Color.new(64, 0, @current_color%255) if hand.eql? :left
|
|
180
|
+
color = Qt::Color.new(@current_color%255, 0, 64) if hand.eql? :right
|
|
181
|
+
#change color for next delta
|
|
182
|
+
@current_color += 16
|
|
183
|
+
ty = 7*@height/12
|
|
184
|
+
path = Qt::PainterPath.new
|
|
185
|
+
path.moveTo 0,0
|
|
186
|
+
#now we construct the path
|
|
187
|
+
@graph.points.each do |ts,v|
|
|
188
|
+
path.lineTo v[hand][:deltas][delta][:point]
|
|
189
|
+
end
|
|
190
|
+
#and return the values
|
|
191
|
+
return { :y => ty, :name => name, :path => path, :color => color }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
#gets called when a repaint is necessary
|
|
195
|
+
def paintEvent(event)
|
|
196
|
+
paint_graphs
|
|
197
|
+
paint_text
|
|
198
|
+
paint_legend
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
#outsourced graph painting
|
|
202
|
+
def paint_graphs
|
|
203
|
+
p = Qt::Painter.new(self)
|
|
204
|
+
p.setRenderHint(Qt::Painter::HighQualityAntialiasing)
|
|
205
|
+
p.setBrush Qt::NoBrush
|
|
206
|
+
@graphs.each do |name,path|
|
|
207
|
+
p.resetMatrix
|
|
208
|
+
p.translate((@width/2)-@xdelta,path[:y])
|
|
209
|
+
pen = Qt::Pen.new(Qt::SolidLine)
|
|
210
|
+
pen.setColor path[:color]
|
|
211
|
+
if path.has_key? :width
|
|
212
|
+
pen.setWidth path[:width]
|
|
213
|
+
else
|
|
214
|
+
pen.setWidth 1.5
|
|
215
|
+
end
|
|
216
|
+
p.setPen(pen)
|
|
217
|
+
p.drawPath path[:path]
|
|
218
|
+
end
|
|
219
|
+
p.end()
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
#outsourced text painting
|
|
223
|
+
def paint_text
|
|
224
|
+
p = Qt::Painter.new(self)
|
|
225
|
+
p.setRenderHint(Qt::Painter::HighQualityAntialiasing)
|
|
226
|
+
p.setBrush Qt::NoBrush
|
|
227
|
+
p.resetMatrix
|
|
228
|
+
p.translate((@width/2)-@xdelta,@text[:y])
|
|
229
|
+
pen = Qt::Pen.new(Qt::SolidLine)
|
|
230
|
+
pen.setWidth 2
|
|
231
|
+
p.setPen(pen)
|
|
232
|
+
p.drawPath @text[:path]
|
|
233
|
+
p.end()
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
#outsourced legend painting
|
|
237
|
+
def paint_legend
|
|
238
|
+
p = Qt::Painter.new(self)
|
|
239
|
+
p.setRenderHint(Qt::Painter::HighQualityAntialiasing)
|
|
240
|
+
p.resetMatrix
|
|
241
|
+
p.translate(0,0)
|
|
242
|
+
p.setBrush @brush
|
|
243
|
+
pen = Qt::Pen.new(Qt::SolidLine)
|
|
244
|
+
pen.setWidth 1
|
|
245
|
+
p.setPen(pen)
|
|
246
|
+
p.drawPath @legend
|
|
247
|
+
p.end()
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
#mouse press event
|
|
251
|
+
def mousePressEvent event
|
|
252
|
+
if event.buttons == Qt::RightButton
|
|
253
|
+
elsif event.buttons == Qt::LeftButton
|
|
254
|
+
#update
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
#a mouse move event
|
|
259
|
+
def mouseMoveEvent event
|
|
260
|
+
@mousex,@mousey = event.x,event.y
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
#ruby thread test
|
|
264
|
+
def reactive_scroll
|
|
265
|
+
fps = 60.0 #frames per second we attempt to compute
|
|
266
|
+
tts = 0.0 #time to sleep
|
|
267
|
+
last_time = Time.now
|
|
268
|
+
Thread.new do
|
|
269
|
+
loop do
|
|
270
|
+
tts = 1.0/fps - (Time.now-last_time)
|
|
271
|
+
sleep tts if tts > 0
|
|
272
|
+
unless @mousex.nil?
|
|
273
|
+
scroll_x
|
|
274
|
+
smooth_delta
|
|
275
|
+
if @xspeed.abs > 1
|
|
276
|
+
#are we speeding?
|
|
277
|
+
if @xspeed < -1.0*@speedlimit
|
|
278
|
+
@xspeed = -1.0*@speedlimit
|
|
279
|
+
@xgoal = @xdelta
|
|
280
|
+
end
|
|
281
|
+
if @xspeed > @speedlimit
|
|
282
|
+
@xspeed = @speedlimit
|
|
283
|
+
@xgoal = @xdelta
|
|
284
|
+
end
|
|
285
|
+
#new delta
|
|
286
|
+
@xdelta = (@xdelta + @xspeed).to_i
|
|
287
|
+
update
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
last_time = Time.now
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
#scroll on the x_axis if necessary
|
|
296
|
+
def scroll_x
|
|
297
|
+
damper = 10
|
|
298
|
+
#need some kind of zone that triggers scrolling: 1/4 width left and right
|
|
299
|
+
if @mousex < @width/4
|
|
300
|
+
@xgoal += (@mousex - @width/4)/damper
|
|
301
|
+
end
|
|
302
|
+
if @mousex > 3*@width/4
|
|
303
|
+
@xgoal += (@mousex - 3*@width/4)/damper
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
#method that smoothes the scroll deltas over time
|
|
308
|
+
def smooth_delta
|
|
309
|
+
@xspeed = (@xgoal-@xdelta)*0.05
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
#start the qt application
|
|
314
|
+
app = Qt::Application.new(ARGV)
|
|
315
|
+
gui = Gui.new(1200,760)
|
|
316
|
+
#dirty qt timer magic to make ruby threads work
|
|
317
|
+
block=Proc.new{ Thread.pass }
|
|
318
|
+
timer=Qt::Timer.new(gui)
|
|
319
|
+
invoke=Qt::BlockInvocation.new(timer, block, "invoke()")
|
|
320
|
+
Qt::Object.connect(timer, SIGNAL("timeout()"), invoke, SLOT("invoke()"))
|
|
321
|
+
timer.start(12.5) #in millis
|
|
322
|
+
#end of dirty timer hack
|
|
323
|
+
gui.show()
|
|
324
|
+
app.exec()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
This class is used to generate test Tracker-Data to validate and debug
|
|
3
|
+
the other software.
|
|
4
|
+
=end
|
|
5
|
+
|
|
6
|
+
require "rubygems"
|
|
7
|
+
require "narray"
|
|
8
|
+
require "sequel"
|
|
9
|
+
require "rofl"
|
|
10
|
+
|
|
11
|
+
#connect to the database
|
|
12
|
+
DB = Sequel.connect(:adapter=>'mysql', :host=>'localhost', :database=>'database', :user=>'user', :password=>'password')
|
|
13
|
+
|
|
14
|
+
class TrackerTestDataGenerator
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
tablename = "TrackerTest"
|
|
18
|
+
#drop old test tables
|
|
19
|
+
DB.drop_table tablename.to_sym
|
|
20
|
+
#create new table for test data
|
|
21
|
+
create_test_table tablename
|
|
22
|
+
#our access to the db is here
|
|
23
|
+
@rows = DB[tablename.to_sym]
|
|
24
|
+
@freq = 50.0 #data frequency in Hz
|
|
25
|
+
length = 60.0 #in seconds
|
|
26
|
+
generate_test_data length
|
|
27
|
+
ilog "Generated #{@rows.count} test frames."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#this method will generate some easy to debug test tracker data for Neck,Left Hand,Right Hand
|
|
31
|
+
def generate_test_data length
|
|
32
|
+
chunk = 1.0 / @freq #frame distance in seconds
|
|
33
|
+
frames = length * @freq #number of frames per identifier
|
|
34
|
+
#now we generate the test frames
|
|
35
|
+
(0..frames).each do |i|
|
|
36
|
+
timestamp = i*chunk
|
|
37
|
+
m = NMatrix.float(4,4).unit
|
|
38
|
+
#identifier specific changes
|
|
39
|
+
write_matrix_to_db timestamp,"Neck",m
|
|
40
|
+
write_matrix_to_db timestamp,"Left_Hand",get_left_hand(i)
|
|
41
|
+
write_matrix_to_db timestamp,"Right_Hand",get_right_hand(i)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#generate test data for left hand
|
|
46
|
+
def get_left_hand index
|
|
47
|
+
m = NMatrix.float(4,4).unit
|
|
48
|
+
#Left hand changes to position vector
|
|
49
|
+
m[3,0] = -1 #x
|
|
50
|
+
m[3,1] = 0.2 #y
|
|
51
|
+
m[3,2] = -0.5 #z
|
|
52
|
+
#make potential BOH point to BAB
|
|
53
|
+
rot_x = x_rotation_matrix -0.5 #rotate 90 degrees around x axis
|
|
54
|
+
m *= rot_x
|
|
55
|
+
#and now rotate around BOH axis at 1Hz
|
|
56
|
+
rot_y = y_rotation_matrix(-index*(2.0/@freq))
|
|
57
|
+
m *= rot_y
|
|
58
|
+
return m
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#generate test data for right hand
|
|
62
|
+
def get_right_hand index
|
|
63
|
+
m = NMatrix.float(4,4).unit
|
|
64
|
+
#Right hand changes to position vector
|
|
65
|
+
m[3,0] = 1 #x
|
|
66
|
+
m[3,1] = 0.2 #y
|
|
67
|
+
m[3,2] = -0.5 #z
|
|
68
|
+
#make potential BOH point to BAB
|
|
69
|
+
rot_x = x_rotation_matrix -0.5 #rotate 90 degrees around x axis
|
|
70
|
+
m *= rot_x
|
|
71
|
+
#and now rotate around BOH axis at 1Hz
|
|
72
|
+
rot_y = y_rotation_matrix(index*(2.0/@freq))
|
|
73
|
+
m *= rot_y
|
|
74
|
+
return m
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#get a rotation matrix for a rotation around the x axis
|
|
78
|
+
def x_rotation_matrix radian
|
|
79
|
+
m = NMatrix.float(4,4).unit
|
|
80
|
+
m[1,1] = Math.cos(radian*Math::PI)
|
|
81
|
+
m[1,2] = Math.sin(radian*Math::PI)
|
|
82
|
+
m[2,1] = Math.sin(radian*Math::PI)*(-1.0)
|
|
83
|
+
m[2,2] = Math.cos(radian*Math::PI)
|
|
84
|
+
return m
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#get a rotation matrix for a rotation around the y axis
|
|
88
|
+
def y_rotation_matrix radian
|
|
89
|
+
m = NMatrix.float(4,4).unit
|
|
90
|
+
m[0,0] = Math.cos(radian*Math::PI)
|
|
91
|
+
m[0,2] = Math.sin(radian*Math::PI)*(-1.0)
|
|
92
|
+
m[2,0] = Math.sin(radian*Math::PI)
|
|
93
|
+
m[2,2] = Math.cos(radian*Math::PI)
|
|
94
|
+
return m
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
#get a rotation matrix for a rotation around the z axis
|
|
98
|
+
def z_rotation_matrix radian
|
|
99
|
+
m = NMatrix.float(4,4).unit
|
|
100
|
+
m[0,0] = Math.cos(radian*Math::PI)
|
|
101
|
+
m[0,1] = Math.sin(radian*Math::PI)
|
|
102
|
+
m[1,0] = Math.sin(radian*Math::PI)*(-1.0)
|
|
103
|
+
m[1,1] = Math.cos(radian*Math::PI)
|
|
104
|
+
return m
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
#write matrix to db
|
|
108
|
+
def write_matrix_to_db timestamp,identifier,matrix
|
|
109
|
+
m = matrix.transpose #matrix data in the db are transposed
|
|
110
|
+
#insert the matrix into the db
|
|
111
|
+
@rows.insert(:timestamp => timestamp, :identifier => identifier,
|
|
112
|
+
:xpos => m[0,3]*1000,:ypos => m[1,3]*1000,:zpos => m[2,3]*1000,
|
|
113
|
+
:m00 => m[0,0],:m10 => m[1,0],:m20 => m[2,0],:m30 => m[3,0],
|
|
114
|
+
:m01 => m[0,1],:m11 => m[1,1],:m21 => m[2,1],:m31 => m[3,1],
|
|
115
|
+
:m02 => m[0,2],:m12 => m[1,2],:m22 => m[2,2],:m32 => m[3,2],
|
|
116
|
+
:m03 => m[0,3],:m13 => m[1,3],:m23 => m[2,3],:m33 => m[3,3])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
#create table to store test data into
|
|
120
|
+
def create_test_table tablename
|
|
121
|
+
DB.create_table tablename.to_sym do
|
|
122
|
+
primary_key :index
|
|
123
|
+
String :identifier
|
|
124
|
+
Double :timestamp
|
|
125
|
+
Float :xpos
|
|
126
|
+
Float :ypos
|
|
127
|
+
Float :zpos
|
|
128
|
+
#unity matrix is default
|
|
129
|
+
Float :m00, :default => 1.0
|
|
130
|
+
Float :m01, :default => 0.0
|
|
131
|
+
Float :m02, :default => 0.0
|
|
132
|
+
Float :m03, :default => 0.0
|
|
133
|
+
Float :m10, :default => 0.0
|
|
134
|
+
Float :m11, :default => 1.0
|
|
135
|
+
Float :m12, :default => 0.0
|
|
136
|
+
Float :m13, :default => 0.0
|
|
137
|
+
Float :m20, :default => 0.0
|
|
138
|
+
Float :m21, :default => 0.0
|
|
139
|
+
Float :m22, :default => 1.0
|
|
140
|
+
Float :m23, :default => 0.0
|
|
141
|
+
Float :m30, :default => 0.0
|
|
142
|
+
Float :m31, :default => 0.0
|
|
143
|
+
Float :m32, :default => 0.0
|
|
144
|
+
Float :m33, :default => 1.0
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#generate some test data
|
|
150
|
+
test = TrackerTestDataGenerator.new
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pangdudu-mamba
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: "0.1"
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- pangdudu
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-08-27 00:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: pangdudu-rofl
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: "0"
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: sequel
|
|
27
|
+
type: :runtime
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: "0"
|
|
34
|
+
version:
|
|
35
|
+
- !ruby/object:Gem::Dependency
|
|
36
|
+
name: sequel_core
|
|
37
|
+
type: :runtime
|
|
38
|
+
version_requirement:
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: "0"
|
|
44
|
+
version:
|
|
45
|
+
- !ruby/object:Gem::Dependency
|
|
46
|
+
name: sequel_model
|
|
47
|
+
type: :runtime
|
|
48
|
+
version_requirement:
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: "0"
|
|
54
|
+
version:
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: mysql
|
|
57
|
+
type: :runtime
|
|
58
|
+
version_requirement:
|
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: "0"
|
|
64
|
+
version:
|
|
65
|
+
- !ruby/object:Gem::Dependency
|
|
66
|
+
name: narray
|
|
67
|
+
type: :runtime
|
|
68
|
+
version_requirement:
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: "0"
|
|
74
|
+
version:
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: ruby-opengl
|
|
77
|
+
type: :runtime
|
|
78
|
+
version_requirement:
|
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: "0"
|
|
84
|
+
version:
|
|
85
|
+
description: qt-ruby tool for ART tracker data time series analysis.
|
|
86
|
+
email: pangdudu@github
|
|
87
|
+
executables: []
|
|
88
|
+
|
|
89
|
+
extensions: []
|
|
90
|
+
|
|
91
|
+
extra_rdoc_files:
|
|
92
|
+
- README.rdoc
|
|
93
|
+
files:
|
|
94
|
+
- README.rdoc
|
|
95
|
+
- lib/mamba.rb
|
|
96
|
+
- lib/graph.rb
|
|
97
|
+
- lib/b1trackerdata/b1_data_parser.rb
|
|
98
|
+
- lib/b1trackerdata/openglviewer.rb
|
|
99
|
+
- tests/generate_testdata.rb
|
|
100
|
+
has_rdoc: true
|
|
101
|
+
homepage: http://github.com/pangdudu/mamba
|
|
102
|
+
licenses:
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: "0"
|
|
113
|
+
version:
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: "0"
|
|
119
|
+
version:
|
|
120
|
+
requirements: []
|
|
121
|
+
|
|
122
|
+
rubyforge_project:
|
|
123
|
+
rubygems_version: 1.3.5
|
|
124
|
+
signing_key:
|
|
125
|
+
specification_version: 2
|
|
126
|
+
summary: a ruby-qt ART tracker data analysis tool
|
|
127
|
+
test_files: []
|
|
128
|
+
|