kamelopard 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -115,7 +115,6 @@ module Kamelopard
115
115
  # Scientific notation
116
116
  return a.to_f
117
117
  end
118
- p a
119
118
 
120
119
  mult = 1
121
120
  if a =~ /^-/ then
@@ -180,6 +179,12 @@ module Kamelopard
180
179
  # Set it to true to ensure it shows up only in slave mode.
181
180
  attr_reader :master_only
182
181
 
182
+ # Abstract function, designed to take an XML node containing a KML
183
+ # object of this type, and parse it into a Kamelopard object
184
+ def self.parse(x)
185
+ raise "Cannot parse a #{self.class.name}"
186
+ end
187
+
183
188
  # This constructor looks for values in the options hash that match
184
189
  # class attributes, and sets those attributes to the values in the
185
190
  # hash. So a class with an attribute called :when can be set via the
@@ -1772,6 +1777,15 @@ module Kamelopard
1772
1777
  @duration = duration
1773
1778
  end
1774
1779
 
1780
+ def self.parse(x)
1781
+ dur = nil
1782
+ id = x.attributes['id'] if x.attributes? 'id'
1783
+ w.find('//gx:duration').each do |d|
1784
+ dur = d.children[0].to_s.to_f
1785
+ return Wait.new(dur, :id => id)
1786
+ end
1787
+ end
1788
+
1775
1789
  def to_kml(elem = nil)
1776
1790
  k = XML::Node.new 'gx:Wait'
1777
1791
  super k
@@ -169,6 +169,53 @@ module Kamelopard
169
169
  return Constant.new((b.to_f - a.to_f) / 0.0)
170
170
  end
171
171
  end
172
+
173
+ # Interpolates between two points, choosing the shortest great-circle
174
+ # distance between the points.
175
+ class LatLonInterp < FunctionMultiDim
176
+ # a and b are points. This function will yield three variables,
177
+ # twice, expecting the block to return a one-dimensional function
178
+ # interpolating between the first two variables it was sent. The
179
+ # third variable yielded is a symbol, either :latitude or
180
+ # :longitude, to indicate which set of coordinates is being
181
+ # processed.
182
+ attr_reader :latfunc, :lonfunc
183
+
184
+ def initialize(a, b)
185
+ super()
186
+ (lat1, lon1) = [a.latitude, a.longitude]
187
+ (lat2, lon2) = [b.latitude, b.longitude]
188
+
189
+ # if (lat2 - lat1).abs > 90
190
+ # if lat2 > 0
191
+ # lat2 = lat2 - 180
192
+ # else
193
+ # lat2 = lat2 + 180
194
+ # end
195
+ # end
196
+
197
+ @latfunc = yield lat1, lat2, :latitude
198
+
199
+ if (lon2 - lon1).abs > 180
200
+ if lon2 > 0
201
+ lon2 = lon2 - 360
202
+ else
203
+ lon2 = lon2 + 360
204
+ end
205
+ end
206
+
207
+ @lonfunc = yield lon1, lon2, :longitude
208
+ end
209
+
210
+ def run_function(x)
211
+ (lat, lon) = [@latfunc.run_function(x), @lonfunc.run_function(x)]
212
+ lat = lat - 180 if lat > 90
213
+ lat = lat + 180 if lat < -90
214
+ lon = lon - 360 if lon > 180
215
+ lon = lon + 360 if lon < -180
216
+ return [lat, lon]
217
+ end
218
+ end ## End of LatLonInterp
172
219
  end ## End of Functions sub-module
173
220
  end ## End of Kamelopard module
174
221
 
@@ -35,7 +35,7 @@ module Kamelopard
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
38
+ # An array. Each array element is itself an array, containing two
39
39
  # values. The first is associated with a FunctionMultiDim class
40
40
  # representing a multidimensional function. The second is an array of
41
41
  # symbols and nils. Valid symbols include any of the possible
@@ -44,7 +44,8 @@ module Kamelopard
44
44
  # The symbols in the :vals array will be assigned the returned value
45
45
  # corresponding to their position in the :vals array. For instance,
46
46
  # assume the following :multidim argument
47
- # [ { :func => myFunc, :vals = [:latitude, :longitude, nil, :altitude]} ]
47
+ # [ [ myFunc, [:latitude, :longitude, nil, :altitude] ],
48
+ # [ anotherFunc, [:pause] ] ]
48
49
  # When myFunc is evaluated, assume it returns [1, 2, 3, 4, 5]. Thus,
49
50
  # :latitude will be 1, :longitude 2, and so on. Because :vals[2] is
50
51
  # nil, the corresponding element in the results of myFunc will be
@@ -103,7 +104,7 @@ module Kamelopard
103
104
  options[:multidim].each do |md|
104
105
  r = val(md[0], i, p)
105
106
  md[1].each_index do |ind|
106
- hash[md[1][ind]] = r[0, ind] unless md[1][ind].nil?
107
+ hash[md[1][ind]] = r[ind] unless md[1][ind].nil?
107
108
  end
108
109
  end
109
110
  end
@@ -8,20 +8,64 @@ require 'json'
8
8
 
9
9
  # Geocoder base class
10
10
  class Geocoder
11
+ attr_accessor :host, :path, :params
11
12
  def initialize
12
- raise "Unimplemented -- some other class should extend Geocoder and replace this initialize method"
13
+ @params = {}
13
14
  end
14
15
 
16
+ def parse_response(r)
17
+ raise "Unimplemented -- use a child of the Geocoder class"
18
+ end
19
+ end
20
+
21
+ # Some specific geocoding API classes follow. Google's would seem most
22
+ # obvious, but since it requires you to display results on a map, ... I didn't
23
+ # want to have to evaluate other possible restrictions, or require that they be
24
+ # imposed on Kamelopard users.
25
+
26
+ class MapquestGeocoder < Geocoder
27
+ attr_reader :api_key, :response_format
28
+
29
+ def initialize(key, response_format = 'json')
30
+ super()
31
+ @proto = 'http'
32
+ @host = 'www.mapquestapi.com'
33
+ @path = '/geocoding/v1/address'
34
+ @api_key = key
35
+ @response_format = response_format
36
+ @params['key'] = @api_key
37
+ end
38
+
39
+ # Returns an object built from the JSON result of the lookup, or an exception
15
40
  def lookup(address)
16
- raise "Unimplemented -- some other class should extend Geocoder and replace this lookup method"
41
+ # The argument can be a string, in which case PlaceFinder does the parsing
42
+ # The argument can also be a hash, with several possible keys. See the PlaceFinder documentation for details
43
+ # http://developer.yahoo.com/geo/placefinder/guide/requests.html
44
+ http = Net::HTTP.new(@host)
45
+ if address.kind_of? Hash then
46
+ p = @params.merge address
47
+ else
48
+ p = @params.merge( { 'location' => address } )
49
+ end
50
+ q = p.map { |k,v| "#{ k == 'key' ? k : CGI.escape(k) }=#{ k == 'key' ? v : CGI.escape(v) }" }.join('&')
51
+ u = URI::HTTP.build([nil, @host, nil, @path, q, nil])
52
+
53
+ resp = Net::HTTP.get u
54
+ parse_response resp
55
+ end
56
+
57
+ def parse_response(r)
58
+ d = JSON.parse(r)
59
+ raise d['info']['messages'].join(', ') if d['info']['statuscode'] != 0
60
+ d
17
61
  end
18
62
  end
19
63
 
20
64
  # Uses Yahoo's PlaceFinder geocoding service: http://developer.yahoo.com/geo/placefinder/guide/requests.html
21
- # Google's would seem most obvious, but since it requires you to display
22
- # results on a map, ... I didn't want to have to evaluate other possible
23
- # restrictions. The argument to the constructor is a PlaceFinder API key, but
65
+ # The argument to the constructor is a PlaceFinder API key, but
24
66
  # testing suggests it's actually unnecessary
67
+ # NB! This is deprecated, as Yahoo's API is no longer free, and I'm not about to pay them to keep this tested.
68
+ # http://developer.yahoo.com/blogs/ydn/introducing-boss-geo-next-chapter-boss-53654.html
25
69
  class YahooGeocoder < Geocoder
26
70
  def initialize(key)
27
71
  @api_key = key
@@ -109,8 +109,8 @@
109
109
  end
110
110
 
111
111
  # Inserts a KML gx:Wait element
112
- def pause(p)
113
- Kamelopard::Wait.new p
112
+ def pause(p, options = {})
113
+ Kamelopard::Wait.new p, options
114
114
  end
115
115
 
116
116
  # Returns the current Tour object
@@ -161,8 +161,8 @@
161
161
  # Otherwise, it will orbit counter-clockwise. To orbit multiple times, add or
162
162
  # subtract 360 from the endHeading. The tilt argument matches the KML LookAt
163
163
  # tilt argument
164
- def orbit(center, range = 100, tilt = 0, startHeading = 0, endHeading = 360)
165
- fly_to Kamelopard::LookAt.new(center, startHeading, tilt, range), 2, nil
164
+ def orbit(center, range = 100, tilt = 90, startHeading = 0, endHeading = 360)
165
+ am = center.altitudeMode
166
166
 
167
167
  # We want at least 5 points (arbitrarily chosen value), plus at least 5 for
168
168
  # each full revolution
@@ -177,12 +177,14 @@
177
177
  step = step * -1 if startHeading > endHeading
178
178
 
179
179
  lastval = startHeading
180
+ mode = :bounce
180
181
  startHeading.step(endHeading, step) do |theta|
181
182
  lastval = theta
182
- fly_to Kamelopard::LookAt.new(center, theta, tilt, range), 2, nil, 'smooth'
183
+ fly_to Kamelopard::LookAt.new(center, :heading => theta, :tilt => tilt, :range => range, :altitudeMode => am), :duration => 2, :mode => mode
184
+ mode = :smooth
183
185
  end
184
186
  if lastval != endHeading then
185
- fly_to Kamelopard::LookAt.new(center, endHeading, tilt, range), 2, nil, 'smooth'
187
+ fly_to Kamelopard::LookAt.new(center, :heading => endHeading, :tilt => tilt, :range => range, :altitudeMode => am), :duration => 2, :mode => :smooth
186
188
  end
187
189
  end
188
190
 
@@ -502,9 +504,8 @@
502
504
  end
503
505
 
504
506
  # Pulls the Placemarks from the KML document d and yields each in turn to the caller
505
- # k = an XML::Document containing KML
507
+ # d = an XML::Document containing KML
506
508
  def each_placemark(d)
507
- i = 0
508
509
  d.find('//kml:Placemark').each do |p|
509
510
  all_values = {}
510
511
 
@@ -618,9 +619,7 @@
618
619
  # the same time: either LookAt, or Camera
619
620
  #--
620
621
  # 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
622
+ # XXX Make it slow down a bit toward the end of the run
624
623
  #++
625
624
  def bounce(a, b, duration, points, options = {})
626
625
  raise "Arguments to bounce() must either be Camera or LookAt objects, and must be the same type" unless
@@ -634,17 +633,28 @@
634
633
  max_alt = a.altitude
635
634
  max_alt = b.altitude if b.altitude > max_alt
636
635
 
636
+ bounce_alt = 1.3 * (b.altitude - a.altitude).abs
637
+ # 150 is the result of trial-and-error
638
+ gc = 0.8 * great_circle_distance(a, b) * 150
639
+ bounce_alt = gc if gc > bounce_alt
640
+ #raise "wtf: #{a.inspect}, #{b.inspect}"
641
+
642
+ latlonfunc = LatLonInterp.new(a, b) do |x, y, z|
643
+ Line.interpolate(x, y)
644
+ end
645
+
637
646
  opts = {
638
647
  :latitude => Line.interpolate(a.latitude, b.latitude),
639
648
  :longitude => Line.interpolate(a.longitude, b.longitude),
649
+ :multidim => [[ latlonfunc, [ :latitude, :longitude ]]],
640
650
  :heading => Line.interpolate(a.heading, b.heading),
641
651
  :tilt => Line.interpolate(a.tilt, b.tilt),
642
652
  # XXX This doesn't really work. An actual altitude requires a
643
653
  # value, and a mode, and we ignore the modes because there's no
644
654
  # way for us to figure out absolute altitudes given, say,
645
655
  # :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)
656
+ # ymin, ymax x1 y1
657
+ :altitude => Quadratic.interpolate(a.altitude, b.altitude, 0.5, bounce_alt),
648
658
  :altitudeMode => a.altitudeMode,
649
659
  :duration => duration * 1.0 / points,
650
660
  }
@@ -658,3 +668,20 @@
658
668
  end
659
669
  return make_function_path(points, opts)
660
670
  end
671
+
672
+ # Returns the great circle distance between two points
673
+ def great_circle_distance(a, b)
674
+ # Stolen from http://rosettacode.org/wiki/Haversine_formula#Ruby
675
+ include Math
676
+
677
+ def deg2rad(a)
678
+ a * PI / 180
679
+ end
680
+
681
+ radius = 6371 # rough radius of the Earth, in kilometers
682
+ lat1, long1 = [Math::PI * a.latitude / 180.0, Math::PI * a.longitude / 180.0]
683
+ lat2, long2 = [Math::PI * b.latitude / 180.0, Math::PI * b.longitude / 180.0]
684
+ d = 2 * radius * asin(sqrt(sin((lat2-lat1)/2)**2 + cos(lat1) * cos(lat2) * sin((long2 - long1)/2)**2))
685
+
686
+ return d
687
+ end
@@ -101,8 +101,6 @@ module Kamelopard
101
101
  f << pl
102
102
  end
103
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
104
  def self.get_camera(heading, tilt, roll, cam_num, cam_angle, cam_count = nil)
107
105
  if cam_angle.nil? then
108
106
  cam_angle = cam_num * 360.0 / cam_count
@@ -121,6 +119,14 @@ module Kamelopard
121
119
  return [h, t, -1 * r]
122
120
  end
123
121
 
122
+ def self.get_camera_view(v, cam_num, cam_angle, cam_count = nil)
123
+ (h, t, r) = get_camera(v.heading, v.tilt, v.roll, cam_num, cam_angle, cam_count)
124
+ v.heading = h
125
+ v.tilt = t
126
+ v.roll = r
127
+ v
128
+ end
129
+
124
130
  def self.test(kml_name = 'multicam_test.kml')
125
131
  name_document 'tourvid'
126
132
  get_document().open = 1
@@ -0,0 +1,51 @@
1
+ #!env ruby
2
+
3
+ # The idea here is to ingest one tour from one KML document, and make it
4
+ # multicamera-ish
5
+
6
+ #require 'rubygems'
7
+ $LOAD_PATH << './lib'
8
+ require 'kamelopard'
9
+ require 'libxml'
10
+
11
+ # command line options: a single kml file. Prints KML to stdout
12
+
13
+ def process_wait(w)
14
+ Kamelopard::Wait.parse(w)
15
+ end
16
+
17
+ def process_flyto(f)
18
+ # The idea here is to check the flytomode. If it's "smooth", just fly to
19
+ # that point. If it's "bounce", use bounce() to simulate the flyto. Since
20
+ # bounce() requires a start and an end, we'll need to keep track of where
21
+ # we are, so if the next flyto is a bounce, we have a start point.
22
+ # XXX Question: What if the *first* flyto is a bounce? We won't have a
23
+ # start point. Throw an error?
24
+ # XXX Should we assume a default value for flyToMode, if we don't find one?
25
+ f.find('//gx:flyToMode').each do |m|
26
+ if m.children[0].to_s == 'smooth'
27
+ break
28
+ end
29
+ end
30
+
31
+ d = XML::Document.file(ARGV[0])
32
+ tours = d.find('//gx:Tour')
33
+ if tours.size > 1 then
34
+ STDERR.puts "Found multiple tours in this document. Processing only the first."
35
+ # XXX Fix this
36
+ elsif tours.size == 0 then
37
+ STDERR.puts "Found no tours in the document. Error."
38
+ exit 1
39
+ end
40
+
41
+ tour = tours[0]
42
+
43
+ tour.find('//gx:FlyTo|//gx:Wait').each do |n|
44
+ if n.name == 'Wait'
45
+ Kamelopard::Wait.parse(w)
46
+ elsif n.name == 'FlyTo'
47
+ process_flyto n
48
+ end
49
+ end
50
+
51
+ puts get_kml.to_s
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.12
4
+ version: 0.0.13
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -22,6 +22,7 @@ extensions: []
22
22
  extra_rdoc_files: []
23
23
  files:
24
24
  - lib/kamelopard/multicam.rb
25
+ - lib/kamelopard/multicamify.rb
25
26
  - lib/kamelopard/geocode.rb
26
27
  - lib/kamelopard/classes.rb
27
28
  - lib/kamelopard/helpers.rb