kamelopard 0.0.12 → 0.0.13

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