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