kamelopard 0.0.11 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,3 +3,4 @@ require 'kamelopard/helpers'
3
3
  require 'kamelopard/geocode'
4
4
  require 'kamelopard/function'
5
5
  require 'kamelopard/function_paths'
6
+ require 'kamelopard/multicam'
@@ -108,6 +108,15 @@ module Kamelopard
108
108
  def Kamelopard.convert_coord(a) # :nodoc:
109
109
  a = a.to_s.upcase.strip.gsub(/\s+/, '')
110
110
 
111
+ if a =~ /^[+-]?\d+(\.\d+)?$/ then
112
+ # coord needs no transformation
113
+ return a.to_f
114
+ elsif a =~ /^[+-]?\d+\.\d+E?-?\d+/ then
115
+ # Scientific notation
116
+ return a.to_f
117
+ end
118
+ p a
119
+
111
120
  mult = 1
112
121
  if a =~ /^-/ then
113
122
  mult *= -1
@@ -121,10 +130,7 @@ module Kamelopard
121
130
  a = a.sub /[NESW]$/, ''
122
131
  a = a.strip
123
132
 
124
- if a =~ /^\d+(\.\d+)?$/ then
125
- # coord needs no transformation
126
- 1
127
- elsif a =~ /^\d+D\d+M\d+(\.\d+)?S$/ then
133
+ if a =~ /^\d+D\d+M\d+(\.\d+)?S$/ then
128
134
  # coord is in dms
129
135
  p = a.split /[D"']/
130
136
  a = p[0].to_f + (p[2].to_f / 60.0 + p[1].to_f) / 60.0
@@ -143,7 +149,7 @@ module Kamelopard
143
149
  # check that it's within range
144
150
  a = a.to_f * mult
145
151
  raise "Coordinate #{a} out of range" if a > 180 or a < -180
146
- return a
152
+ return a.to_f
147
153
  end
148
154
 
149
155
  # Helper function for altitudeMode / gx:altitudeMode elements
@@ -629,11 +635,13 @@ module Kamelopard
629
635
  end
630
636
 
631
637
  def roll
638
+ # The roll element doesn't exist in LookAt objects
632
639
  raise "The roll element is part of Camera objects, not LookAt objects"
633
640
  end
634
641
 
635
- def roll=
642
+ def roll=(a)
636
643
  # The roll element doesn't exist in LookAt objects
644
+ raise "The roll element is part of Camera objects, not LookAt objects"
637
645
  end
638
646
  end
639
647
 
@@ -14,7 +14,7 @@ module Kamelopard
14
14
  module Functions
15
15
 
16
16
  # Abstract class representing a one-dimensional function
17
- class Function1D
17
+ class Function
18
18
  # min and max describe the function's domain. Values passed to
19
19
  # get_value will only range from 0 to 1; the actual value
20
20
  # calculated will be mapped to a percentage of that domain.
@@ -42,7 +42,7 @@ module Kamelopard
42
42
  end
43
43
 
44
44
  def compose=(f)
45
- raise "Can only compose another one-dimensional function" unless f.kind_of? Function1D or f.nil?
45
+ raise "Can only compose another function" unless f.kind_of? Function or f.nil?
46
46
  @compose = f
47
47
  end
48
48
 
@@ -57,7 +57,7 @@ module Kamelopard
57
57
  end
58
58
 
59
59
  #def append(f)
60
- # raise "Can only append another one-dimensional function" unless f.kind_of? Function1D or f.nil?
60
+ # raise "Can only append another one-dimensional function" unless f.kind_of? Function or f.nil?
61
61
  # print STDERR "WARNING: append() isn't actually implemented" unless f.nil?
62
62
  # # XXX
63
63
  # # Gotta implement this. The idea is to have one function for the first
@@ -74,9 +74,29 @@ module Kamelopard
74
74
  end
75
75
 
76
76
  def self.interpolate(a, b)
77
- # Creates a new Function1D object between points A and B
77
+ # Creates a new Function object between points A and B
78
78
  raise "Override this method before calling it, please"
79
79
  end
80
+ end ## End of Function class
81
+
82
+ # get_value and run_function return a single scalar value
83
+ class Function1D < Function
84
+ def compose=(f)
85
+ raise "Can only compose another one-dimensional function" unless f.kind_of? Function1D or f.nil?
86
+ @compose = f
87
+ end
88
+
89
+ end
90
+
91
+ # get_value and run_function return an array of values
92
+ class FunctionMultiDim < Function
93
+ attr_reader :ndims
94
+
95
+ def compose=(f)
96
+ raise "Can only compose another #{@ndims}-dimensional function" unless (f.kind_of? FunctionMultiDim and @ndims = f.ndims) or f.nil?
97
+ @compose = f
98
+ end
99
+
80
100
  end
81
101
 
82
102
  # Represents a cubic equation of the form c3 * x^3 + c2 * x^2 + c1 * x + c0
@@ -105,7 +125,7 @@ module Kamelopard
105
125
  c0 = m[0,3]
106
126
  return Cubic.new(c3, c2, c1, c0, min, max)
107
127
  end
108
- end
128
+ end ## End of Cubic class
109
129
 
110
130
  # Describes a quadratic equation
111
131
  class Quadratic < Cubic
@@ -149,8 +169,11 @@ module Kamelopard
149
169
  return Constant.new((b.to_f - a.to_f) / 0.0)
150
170
  end
151
171
  end
152
- end
153
- end
172
+ end ## End of Functions sub-module
173
+ end ## End of Kamelopard module
174
+
175
+ ## Example uses
176
+
154
177
  # include Kamelopard::Functions
155
178
  #
156
179
  # l = Line.new 1.0, 0.0
@@ -13,7 +13,7 @@ module Kamelopard
13
13
  # points: The number of points in the series
14
14
  # hash: Values used to create the hash, which creates the point in the
15
15
  # series. Keys in this hash include:
16
- # latitude, longitude, altitude, heading, tilt, roll, range, duration, altitudeMode, extrude
16
+ # Any option suitable for the make_view_from() function
17
17
  # These can be constant numbers, Proc objects, or Function1D objects.
18
18
  # The latter two will be called once for each point in the series.
19
19
  # Proc objects will be passed the number of the point they're
@@ -21,7 +21,7 @@ module Kamelopard
21
21
  # created for this point. "duration" represents the time in seconds
22
22
  # spent flying from the last point to this one.
23
23
  # callback
24
- # This Proc object, if defined, will be called after the above hash
24
+ # This Proc object, if defined, will be called after the other hash
25
25
  # keys have been calculated. It gets passed the number of the point,
26
26
  # and the current value of the hash for this point. It can modify and
27
27
  # return that hash as needed.
@@ -34,11 +34,33 @@ module Kamelopard
34
34
  # If set, a placemark object will be created at this point
35
35
  # no_flyto
36
36
  # If set, on flyto objects will be created
37
-
37
+ # multidim
38
+ # An array of hashes. Each array element is an array, containing two
39
+ # values. The first is associated with a FunctionMultiDim class
40
+ # representing a multidimensional function. The second is an array of
41
+ # symbols and nils. Valid symbols include any of the possible
42
+ # make_function_path options, except :multidim. At execution, the
43
+ # FunctionMultiDim will be evaluated, returning an array of values.
44
+ # The symbols in the :vals array will be assigned the returned value
45
+ # corresponding to their position in the :vals array. For instance,
46
+ # assume the following :multidim argument
47
+ # [ { :func => myFunc, :vals = [:latitude, :longitude, nil, :altitude]} ]
48
+ # When myFunc is evaluated, assume it returns [1, 2, 3, 4, 5]. Thus,
49
+ # :latitude will be 1, :longitude 2, and so on. Because :vals[2] is
50
+ # nil, the corresponding element in the results of myFunc will be
51
+ # ignored. Also, given that :vals contains four values whereas myFunc
52
+ # returned 5, the unallocated final myFunc value will also be
53
+ # ignored.
54
+ # NOTE ON PROCESSING ORDER
55
+ # Individually specified hash options are processed first, followed by
56
+ # :multidim. So hash options included directly as
57
+ # well as in a :multidim :vals array will take the value from
58
+ # :multidim. make_function_path yields to code blocks last, after all
59
+ # other assignment.
38
60
  def make_function_path(points = 10, options = {})
39
61
 
40
62
  def val(a, b, c) # :nodoc:
41
- if a.kind_of? Function1D then
63
+ if a.kind_of? Function then
42
64
  return a.get_value(c)
43
65
  elsif a.kind_of? Proc then
44
66
  return a.call(b, a)
@@ -47,50 +69,92 @@ module Kamelopard
47
69
  end
48
70
  end
49
71
 
50
- result_points = []
72
+ views = []
73
+ placemarks = []
51
74
 
52
75
  callback_value = nil
53
76
  i = 0
54
77
  while (i <= points)
55
78
  p = i.to_f / points.to_f
56
- hash = {
57
- :latitude => val(options[:latitude], i, p),
58
- :longitude => val(options[:longitude], i, p),
59
- :altitude => val(options[:altitude], i, p),
60
- :heading => val(options[:heading], i, p),
61
- :tilt => val(options[:tilt], i, p),
62
- :altitudeMode => val(options[:altitudeMode], i, p),
63
- :extrude => val(options[:extrude], i, p),
64
- }
79
+ hash = {}
80
+ [ :latitude, :longitude, :altitude, :heading,
81
+ :tilt, :altitudeMode, :extrude, :when,
82
+ :roll, :range, :pause, :begin, :end, :show_placemarks,
83
+ :no_flyto, :pause
84
+ ].each do |k|
85
+ if options.has_key? k then
86
+ hash[k] = val(options[k], i, p)
87
+ end
88
+ end
65
89
 
66
90
  hash[:show_placemarks] = options[:show_placemarks] if options.has_key? :show_placemarks
67
- hash[:roll] = val(options[:roll], i, p) if options.has_key? :roll
68
- hash[:range] = val(options[:range], i, p) if options.has_key? :range
69
- hash[:pause] = val(options[:pause], i, p) if options.has_key? :pause
91
+ #hash[:roll] = val(options[:roll], i, p) if options.has_key? :roll
92
+ #hash[:range] = val(options[:range], i, p) if options.has_key? :range
93
+ #hash[:pause] = val(options[:pause], i, p) if options.has_key? :pause
70
94
 
71
- if hash.has_key? :duration
95
+ if options.has_key? :duration
72
96
  duration = val(options[:duration], i, p)
73
97
  else
74
98
  duration = (i == 0 ? 0 : 2)
75
99
  end
100
+ hash[:duration] = duration
101
+
102
+ if options.has_key? :multidim then
103
+ options[:multidim].each do |md|
104
+ r = val(md[0], i, p)
105
+ md[1].each_index do |ind|
106
+ hash[md[1][ind]] = r[0, ind] unless md[1][ind].nil?
107
+ end
108
+ end
109
+ end
76
110
 
77
111
  hash[:callback_value] = callback_value unless callback_value.nil?
78
- tmp = yield(i, hash)
79
- hash = tmp unless tmp.nil?
112
+
113
+ begin
114
+ tmp = yield(i, hash)
115
+ hash = tmp unless tmp.nil?
116
+ rescue LocalJumpError
117
+ # Don't do anything; there's no block to yield to
118
+ end
80
119
  #hash = options[:callback].call(i, hash) if options.has_key? :callback
81
120
  callback_value = hash[:callback_value] if hash.has_key? :callback_value
82
121
 
83
122
  v = make_view_from(hash)
84
123
  p = point(v.longitude, v.latitude, v.altitude, hash[:altitudeMode], hash[:extrude])
85
- get_folder << placemark(i.to_s, :geometry => p) if hash.has_key? :show_placemarks
124
+ # XXX Should I add the view's timestamp / timespan, if it exists, to the placemark?
125
+ pl = placemark(i.to_s, :geometry => p)
126
+ pl.abstractView = v
127
+ get_folder << pl if hash.has_key? :show_placemarks
86
128
  fly_to v, :duration => duration , :mode => :smooth unless hash.has_key? :no_flyto
87
- result_points << v
129
+ views << v
130
+ placemarks << pl
88
131
 
89
132
  pause hash[:pause] if hash.has_key? :pause
90
133
 
91
134
  i = i + 1
92
135
  end
93
- result_points
136
+ [views, placemarks]
94
137
  end
95
138
 
96
139
  end
140
+
141
+ ## Example
142
+ #make_function_path(10,
143
+ # :latitude => Line.interpolate(38.8, 40.3),
144
+ # :altitude => Line.interpolate(10000, 2000),
145
+ # :heading => Line.interpolate(0, 90),
146
+ # :tilt => Line.interpolate(40.0, 90),
147
+ # :roll => 0,
148
+ # :show_placemarks => 1,
149
+ # :duration => Quadratic.interpolate(2.0, 4.0, 0.0, 1.0),
150
+ #) do |a, v|
151
+ # puts "callback here"
152
+ # if v.has_key? :callback_value then
153
+ # v[:callback_value] += 1
154
+ # else
155
+ # v[:pause] = 0.01
156
+ # v[:callback_value] = 1
157
+ # end
158
+ # puts v[:callback_value]
159
+ # v
160
+ #end
@@ -401,15 +401,21 @@
401
401
 
402
402
  # Given a hash of values, this creates an AbstractView object. Possible
403
403
  # values in the hash are :latitude, :longitude, :altitude, :altitudeMode,
404
- # :tilt, :heading, :roll, and :range. If the hash specifies :roll, a Camera
405
- # object will result; otherwise, a LookAt object will result. Specifying both
406
- # :roll and :range will still result in a Camera object, and the :range
407
- # option will be ignored. :roll and :range have no default; all other values
408
- # default to 0 except :altitudeMode, which defaults to :relativeToGround
404
+ # :tilt, :heading, :roll, :range, :begin, :end, and :when. If the hash
405
+ # specifies :roll, a Camera object will result; otherwise, a LookAt object
406
+ # will result. Specifying both :roll and :range will still result in a Camera
407
+ # object, and the :range option will be ignored.
408
+ #
409
+ # :begin, :end, and :when are used to create the view's timestamp or timespan
410
+ #
411
+ # :roll, :range, and the timestamp / timespan options have no default; all
412
+ # other values default to 0 except :altitudeMode, which defaults to
413
+ # :relativeToGround.
409
414
  def make_view_from(options = {})
410
415
  o = {}
411
416
  o.merge! options
412
- options.each do |k, v| o[k.to_sym] = v unless k.kind_of? Symbol
417
+ options.each do |k, v|
418
+ o[k.to_sym] = v unless k.kind_of? Symbol
413
419
  end
414
420
 
415
421
  # Set defaults
@@ -432,8 +438,18 @@
432
438
  else
433
439
  view = Kamelopard::LookAt.new p
434
440
  end
441
+
442
+ if o.has_key? :when then
443
+ o[:timestamp] = Kamelopard::TimeStamp.new(o[:when])
444
+ elsif o.has_key? :begin or o.has_key? :end then
445
+ (b, e) = [nil, nil]
446
+ b = o[:begin] if o.has_key? :begin
447
+ e = o[:end] if o.has_key? :end
448
+ o[:timespan] = Kamelopard::TimeSpan.new(b, e)
449
+ end
435
450
 
436
- [ :altitudeMode, :tilt, :heading, :timestamp, :timespan, :timestamp, :range, :roll, :viewerOptions ].each do |a|
451
+ [ :altitudeMode, :tilt, :heading, :timespan, :timestamp, :range, :roll, :viewerOptions ].each do |a|
452
+ #p o[a] if o.has_key? a and a == :timestamp
437
453
  view.method("#{a.to_s}=").call(o[a]) if o.has_key? a
438
454
  end
439
455
 
@@ -596,3 +612,49 @@
596
612
  def get_doc_holder
597
613
  return Kamelopard::DocumentHolder.instance
598
614
  end
615
+
616
+ # Generates a series of points in a path that will simulate Earth's FlyTo in
617
+ # bounce mode, from one view to another. Note that the view objects must be
618
+ # the same time: either LookAt, or Camera
619
+ #--
620
+ # XXX Fix the limitation that the views must be the same type
621
+ # XXX Make the height of the bounce relate to the distance of the travel
622
+ # XXX Make the direction of change for elements that cycle smart enough to
623
+ # choose the shortest direction around the circle
624
+ #++
625
+ def bounce(a, b, duration, points, options = {})
626
+ raise "Arguments to bounce() must either be Camera or LookAt objects, and must be the same type" unless
627
+ ((a.kind_of? Kamelopard::Camera and b.kind_of? Kamelopard::Camera) or
628
+ (a.kind_of? Kamelopard::LookAt and b.kind_of? Kamelopard::LookAt))
629
+ # The idea here is just to generate a function; the hard bit is finding
630
+ # control points.
631
+ include Kamelopard
632
+ include Kamelopard::Functions
633
+
634
+ max_alt = a.altitude
635
+ max_alt = b.altitude if b.altitude > max_alt
636
+
637
+ opts = {
638
+ :latitude => Line.interpolate(a.latitude, b.latitude),
639
+ :longitude => Line.interpolate(a.longitude, b.longitude),
640
+ :heading => Line.interpolate(a.heading, b.heading),
641
+ :tilt => Line.interpolate(a.tilt, b.tilt),
642
+ # XXX This doesn't really work. An actual altitude requires a
643
+ # value, and a mode, and we ignore the modes because there's no
644
+ # way for us to figure out absolute altitudes given, say,
645
+ # :relativeToGround
646
+ :altitude => Quadratic.interpolate(a.altitude, b.altitude, 0.3 / 1.6, 1.3 * (b.altitude - a.altitude).abs),
647
+ # def self.interpolate(ymin, ymax, x1, y1, min = -1.0, max = 1.0)
648
+ :altitudeMode => a.altitudeMode,
649
+ :duration => duration * 1.0 / points,
650
+ }
651
+ opts[:no_flyto] = 1 if options.has_key?(:no_flyto)
652
+ opts[:show_placemarks] = 1 if options.has_key?(:show_placemarks)
653
+
654
+ if a.kind_of? Camera then
655
+ opts[:roll] = Line.interpolate(a.roll, b.roll)
656
+ else
657
+ opts[:range] = Line.interpolate(a.range, b.range)
658
+ end
659
+ return make_function_path(points, opts)
660
+ end
@@ -0,0 +1,181 @@
1
+ require 'matrix'
2
+
3
+ module Kamelopard
4
+ module Multicam
5
+ def self.cross_product(v1, v2)
6
+ x = ( (v1[1] * v2[2]) - (v1[2] * v2[1]) )
7
+ y = - ( (v1[0] * v2[2]) - (v1[2] * v2[0]) )
8
+ z = ( (v1[0] * v2[1]) - (v1[1] * v2[0]) )
9
+ return Vector[x, y, z]
10
+ end
11
+
12
+ def self.dotprod_angle(a, b, negate = false)
13
+ begin
14
+ d = 180.0 * (Math.acos(a.inner_product(b) / a.r / b.r)) / Math::PI
15
+ rescue
16
+ d = 0
17
+ #puts "argument was #{a.inner_product(b)}, from vectors #{a} and #{b}"
18
+ end
19
+ d = 0 if d.respond_to? :nan? and d.nan?
20
+ raise "#{a}, #{b}, #{a.inner_product(b)}" if d == Float::INFINITY
21
+
22
+ # Complicating factor: dot product goes from 0 to 180, not 0 to 360. We'll
23
+ # have to know whether to negate based on external input (like if the
24
+ # original angle involved was close to the threshold)
25
+ d = -d if negate
26
+
27
+ return d
28
+ end
29
+
30
+ def self.rot_x(a)
31
+ # This, and the other two rotation matrix functions, must convert
32
+ # the angle to radians
33
+ a = a * Math::PI / 180.0
34
+ return Matrix[[1, 0, 0], [0, Math.cos(a), -1 * Math.sin(a)], [0, Math.sin(a), Math.cos(a)]]
35
+ end
36
+
37
+ def self.rot_y(a)
38
+ a = a * Math::PI / 180.0
39
+ return Matrix[[Math.cos(a), 0, Math.sin(a)], [0, 1, 0], [-1 * Math.sin(a), 0, Math.cos(a)]]
40
+ end
41
+
42
+ def self.rot_z(a)
43
+ a = a * Math::PI / 180.0
44
+ return Matrix[[Math.cos(a), -1 * Math.sin(a), 0], [Math.sin(a), Math.cos(a), 0], [0, 0, 1]]
45
+ end
46
+
47
+ def self.same_quadrant(a, b)
48
+ (0..2).each do |i|
49
+ return false if (a[i] > 0 and b[i] < 0) or (a[i] < 0 and b[i] > 0)
50
+ end
51
+ return true
52
+ end
53
+
54
+ # Vec is the camera vector. up_vec is the vector out the top of the camera
55
+ def self.vector_to_camera(vec, up_vec)
56
+ # The heading is the angle between two planes, the first formed by the
57
+ # camera vector and the original Z axis, and the second formed by the
58
+ # original Y and Z axes. This angle between two planes is the same
59
+ # as the angle between their two normals. The normal of the first
60
+ # plane is the cross product of the two vector, and that of the
61
+ # second is simply the X axis. The first cross product will be zero
62
+ # if the camera position vector is parallel to the Z axis; in that
63
+ # case we want the angle between the up vector and the y axis
64
+ # This will only find values up to 180 degrees, and won't
65
+ # distinguish direction correctly. For that, look at the Z
66
+ # component of the cross product of the two normals.
67
+ cam_z_norm = cross_product(vec, Vector[0,0,-1])
68
+ if cam_z_norm.r == 0
69
+ heading = dotprod_angle(up_vec, Vector[0,1,0],
70
+ (cross_product(up_vec, Vector[0,1,0])[2] > 0))
71
+ else
72
+ heading = dotprod_angle(cam_z_norm, Vector[1,0,0],
73
+ (cross_product(cam_z_norm, Vector[1,0,0])[2] > 0))
74
+ end
75
+
76
+ # Tilt is calculated from the vector alone, and is the angle between it and
77
+ # the original Z axis, calculated via the dot product
78
+ tilt = dotprod_angle(vec, Vector[0,0,1])
79
+
80
+ # For roll, take the original UP vector, and now that I've got
81
+ # valid heading and tilt, transform it by those values. Take the
82
+ # angle between it and my current UP vector. Make it negative if
83
+ # their cross product, up vector first, isn't the same direction as
84
+ # the camera vector.
85
+ transformed_up = rot_z(heading) * rot_x(tilt) * Vector[0,1,0]
86
+ if cross_product(up_vec, transformed_up).r == 0
87
+ negate = not(same_quadrant(up_vec, transformed_up))
88
+ else
89
+ negate = same_quadrant(vec, cross_product(up_vec, transformed_up))
90
+ end
91
+ roll = dotprod_angle(up_vec, transformed_up, negate)
92
+
93
+ return [heading, tilt, roll]
94
+ end
95
+
96
+ def self.make_placemark(name, lat, lon, alt, tilt, roll, heading)
97
+ p = point(lon, lat, alt, :relativeToGround)
98
+ l = camera p, :heading => heading, :tilt => tilt, :roll => roll, :altitudeMode => :relativeToGround
99
+ pl = placemark(name, :geometry => p, :abstractView => l)
100
+ f = get_folder
101
+ f << pl
102
+ end
103
+
104
+ # XXX Change / augment API, so that this function, or a cognate, takes
105
+ # a view, and returns a view modified for the camera in question
106
+ def self.get_camera(heading, tilt, roll, cam_num, cam_angle, cam_count = nil)
107
+ if cam_angle.nil? then
108
+ cam_angle = cam_num * 360.0 / cam_count
109
+ else
110
+ cam_angle = cam_angle * cam_num
111
+ end
112
+ # The camera vector is [0,0,1] rotated around the Y axis the amount
113
+ # of the camera angle
114
+ camera = rot_y(cam_angle) * Vector[0,0,1]
115
+
116
+ # The up vector is the same for all cameras
117
+ up = Vector[0,1,0]
118
+ matrix = rot_z(heading) * rot_x(tilt) * rot_z(roll)
119
+ (h, t, r) = vector_to_camera(matrix * camera, matrix * up)
120
+ # XXX What am I getting wrong, to require the negated roll?
121
+ return [h, t, -1 * r]
122
+ end
123
+
124
+ def self.test(kml_name = 'multicam_test.kml')
125
+ name_document 'tourvid'
126
+ get_document().open = 1
127
+
128
+ [:roll, :tilt, :heading].each do |which|
129
+ camera = Vector[0,0,1]
130
+ heading = 0
131
+ tilt = 45
132
+ roll = 0
133
+ lat = 40
134
+ lon = -111
135
+ alt = 100
136
+
137
+ puts "------------------"
138
+ puts "Running #{which}"
139
+ folder which.to_s
140
+ get_folder().open = 1
141
+
142
+ up = Vector[0,1,0]
143
+ (0..36).each do |i|
144
+ if which == :roll then
145
+ roll = -180 + i * 10
146
+ heading = 23
147
+ elsif which == :heading then
148
+ heading = i * 10
149
+ heading = heading - 360 if heading >= 180
150
+ else
151
+ tilt = i * 5
152
+ end
153
+
154
+ # This has been verified visually as the right matrix
155
+ matrix = rot_z(heading) * rot_x(tilt) * rot_z(roll)
156
+
157
+ trans_up = matrix * up
158
+ trans_cam = matrix * camera
159
+ trans_cross = cross_product(trans_cam, trans_up)
160
+ (screen_head, screen_tilt, screen_roll) = vector_to_camera(trans_cam, trans_up)
161
+
162
+ diff_limit = 3
163
+ a = dotprod_angle(trans_up, trans_cam)
164
+ if ((heading - screen_head).abs > diff_limit or (tilt - screen_tilt).abs > diff_limit or (roll - screen_roll).abs > diff_limit) then
165
+ #if which == :roll then
166
+ puts " PLACEMARK #{i}"
167
+ # puts " Camera vector: #{trans_cam}, mag: #{trans_cam.r}"
168
+ # puts " Up vector: #{trans_up}, mag: #{trans_up.r}"
169
+ # puts " Cross prod: #{trans_cross}, mag: #{trans_cross.r}"
170
+ puts " UpZ: #{trans_up[2]}"
171
+ puts " Orig H/T/R: #{heading}/#{tilt}/#{roll}"
172
+ puts " Screen H/T/R: #{screen_head}/#{screen_tilt}/#{screen_roll}"
173
+ end
174
+ make_placemark(i, lat, lon, alt, screen_tilt, screen_roll, screen_head)
175
+ end
176
+ puts
177
+ write_kml_to kml_name
178
+ end
179
+ end
180
+ end
181
+ end
@@ -2,56 +2,88 @@
2
2
  require 'matrix'
3
3
 
4
4
  # Basic support for splines
5
-
6
5
  module Kamelopard
7
- def spline(lists = [], resolution = [10])
8
- # Ruby implementation of Catmull-Rom splines (http://www.cubic.org/docs/hermite.htm)
9
-
10
- size = lists.collect { |l| l.size }.max
11
- STDERR.puts size
12
-
13
- h = Matrix[
14
- [ 2, -2, 1, 1 ],
15
- [-3, 3, -2, -1 ],
16
- [ 0, 0, 1, 0 ],
17
- [ 1, 0, 0, 0 ],
18
- ]
19
-
20
- # XXX This needs to be fixed
21
- result = []
22
-
23
- idx = 0
24
- resolution = [resolution] if ! resolution.respond_to? :[]
25
-
26
- # Calculate spline between every two points
27
- (0..(size-2)).each do |i|
28
- p1 = lists_at(lists, i)
29
- p2 = lists_at(lists, i+1)
30
-
31
- # Get surrounding points for calculating tangents
32
- if i <= 0 then pt1 = p1 else pt1 = lists_at(lists, i-1) end
33
- if i >= size - 2 then pt2 = p2 else pt2 = lists_at(lists, i+2) end
34
-
35
- # Build tangent points into matrices to calculate tangents.
36
- t1 = 0.5 * ( Matrix[p2] - Matrix[pt1] )
37
- t2 = 0.5 * ( Matrix[pt2] - Matrix[p1] )
38
-
39
- # Build matrix of Hermite parameters
40
- c = Matrix[p1, p2, t1.row(0), t2.row(0)]
41
-
42
- # Make a set of points
43
- point_count = (resolution[idx] * 1.0 / size).to_i
44
- STDERR.puts point_count
45
- (0..point_count).each do |t|
46
- r = t/10.0
47
- s = Matrix[[r**3, r**2, r, 1]]
48
- tmp = s * h
49
- p = tmp * c
50
- result << p.row(0).to_a
6
+ module Functions
7
+ class SplineFunction < FunctionMultiDim
8
+ attr_reader :control_points, :total_dur, :tension
9
+
10
+ def initialize(ndims, tension = 0.5)
11
+ @ndims = ndims
12
+ @control_points = []
13
+ @total_dur = 0
14
+ @tension = tension
15
+ super()
16
+ end
17
+
18
+ # Adds a new control point. :dur is a way of indicating the
19
+ # duration of the journey from the last point to this one, and is
20
+ # ignored for the first control point in the spline. Values for
21
+ # :dur are in whatever units the user wants; a spline with three
22
+ # control points with durations of 0, 10, and 20 will be identical
23
+ # to one with durations of 0, 1, and 2.
24
+ def add_control_point(point, dur)
25
+ @total_dur = @total_dur + dur if @control_points.size > 0
26
+ @control_points << [ point, dur ]
27
+ end
28
+
29
+ def run_function(x)
30
+ # X will be between 0 and 1
31
+ # Find which control points I should am using for the point in
32
+ # question
33
+
34
+ dur = 0
35
+ last_dur = 0
36
+ cur_i = 0
37
+ u = 0
38
+ @control_points.each_index do |i|
39
+ next if i == 0
40
+ cur_i = i
41
+ last_dur = dur
42
+ if 1.0 * (dur + @control_points[i][1]) / @total_dur >= x then
43
+ # I've found the correct two control points: cp[i-1] and cp[i]
44
+ # u is the point on the interval between the two control points
45
+ # that we're interested in. 0 would be the first control point,
46
+ # and 1 the second
47
+ u = (x * @total_dur - dur) / @control_points[i][1]
48
+ break
49
+ end
50
+ dur = dur + @control_points[i][1]
51
+ end
52
+
53
+ # http://www.cs.cmu.edu/~462/projects/assn2/assn2/catmullRom.pdf
54
+
55
+ # cp = control points. cur_i will be at least 1
56
+ # I need two control points on either side of this part of the
57
+ # spline. If they don't exist, duplicate the endpoints of the
58
+ # control points.
59
+ cp1 = @control_points[cur_i-1][0]
60
+ cp2 = @control_points[cur_i][0]
61
+ if cur_i == 1 then
62
+ cpt1 = cp1
63
+ else
64
+ cpt1 = @control_points[cur_i-2][0]
65
+ end
66
+ if cur_i >= @control_points.size - 1 then
67
+ cpt2 = cp2
68
+ else
69
+ cpt2 = @control_points[cur_i+1][0]
70
+ end
71
+
72
+ # Can't just say Matrix[cp], because that adds an extra
73
+ # dimension to the matrix, somehow.
74
+ cps = Matrix[cpt1, cp1, cp2, cpt2]
75
+
76
+ t = @tension
77
+ h = Matrix[
78
+ [ 0, 1, 0, 0 ],
79
+ [ -t, 0, t, 0 ],
80
+ [ 2*t, t-3, 3-2*t, -t ],
81
+ [ -t, 2-t, t-2, t]
82
+ ]
83
+
84
+ p = Matrix[[1, u, u**2, u**3]] * h * cps
85
+ return p
51
86
  end
52
- idx += 1
53
- idx = 0 if idx >= resolution.size
54
- end
55
- result
56
- end
57
- end
87
+ end ## End of SplineFunction class
88
+ end ## End of Function module
89
+ end ## End of Kamelopard module
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamelopard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.0.12
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-04-15 00:00:00.000000000 Z
13
+ date: 2013-06-18 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: Various classes and functions used to ease development of KML files,
16
16
  in particular for development of Google Earth tours
@@ -21,6 +21,7 @@ executables: []
21
21
  extensions: []
22
22
  extra_rdoc_files: []
23
23
  files:
24
+ - lib/kamelopard/multicam.rb
24
25
  - lib/kamelopard/geocode.rb
25
26
  - lib/kamelopard/classes.rb
26
27
  - lib/kamelopard/helpers.rb