kamelopard 0.0.12 → 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/kamelopard/classes.rb +15 -1
- data/lib/kamelopard/function.rb +47 -0
- data/lib/kamelopard/function_paths.rb +4 -3
- data/lib/kamelopard/geocode.rb +49 -5
- data/lib/kamelopard/helpers.rb +40 -13
- data/lib/kamelopard/multicam.rb +8 -2
- data/lib/kamelopard/multicamify.rb +51 -0
- metadata +2 -1
data/lib/kamelopard/classes.rb
CHANGED
@@ -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
|
data/lib/kamelopard/function.rb
CHANGED
@@ -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
|
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
|
-
# [
|
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[
|
107
|
+
hash[md[1][ind]] = r[ind] unless md[1][ind].nil?
|
107
108
|
end
|
108
109
|
end
|
109
110
|
end
|
data/lib/kamelopard/geocode.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
data/lib/kamelopard/helpers.rb
CHANGED
@@ -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 =
|
165
|
-
|
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,
|
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,
|
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
|
-
#
|
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
|
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
|
-
|
647
|
-
|
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
|
data/lib/kamelopard/multicam.rb
CHANGED
@@ -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.
|
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
|