kamelopard 0.0.11 → 0.0.12

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.
@@ -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