kamelopard 0.0.14 → 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/lib/kamelopard/classes.rb +69 -7
- data/lib/kamelopard/geocode.rb +57 -16
- data/lib/kamelopard/helpers.rb +834 -720
- data/lib/kamelopard/multicamify.rb +2 -0
- data/lib/kamelopard/spline.rb +99 -1
- data/spec/test.rb +2999 -0
- metadata +17 -15
data/lib/kamelopard/helpers.rb
CHANGED
@@ -1,744 +1,858 @@
|
|
1
1
|
#--
|
2
2
|
# vim:ts=4:sw=4:et:smartindent:nowrap
|
3
3
|
#++
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
4
|
+
|
5
|
+
# Kamelopard creates a number of helper functions in the default namespace. In
|
6
|
+
# general, these are probably what users will use most to interact with
|
7
|
+
# Kamelopard, rather than using the classes directly, at least for creating
|
8
|
+
# objects.
|
9
|
+
|
10
|
+
# Returns the current Document object
|
11
|
+
def get_document()
|
12
|
+
Kamelopard::DocumentHolder.instance.current_document
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the singleton Kamelopard::DocumentHolder object
|
16
|
+
def get_doc_holder
|
17
|
+
return Kamelopard::DocumentHolder.instance
|
18
|
+
end
|
19
|
+
|
20
|
+
# Changes the default FlyTo mode. Possible values are :smooth and :bounce
|
21
|
+
def set_flyto_mode_to(mode)
|
22
|
+
Kamelopard::DocumentHolder.instance.current_document.flyto_mode = mode
|
23
|
+
end
|
24
|
+
|
25
|
+
# Clears out all KML documents in memory
|
26
|
+
def clear_documents
|
27
|
+
dh = get_doc_holder
|
28
|
+
dh.delete_current_doc while dh.documents.size > 0
|
29
|
+
end
|
30
|
+
|
31
|
+
# Shows or hides the popup balloon for Placemark and ScreenOverlay objects.
|
32
|
+
# Arguments are the object; 0 or 1 to hide or show the balloon, respectively;
|
33
|
+
# and a hash of options to be added to the AnimatedUpdate object this
|
34
|
+
# function creates. Refer to the AnimatedUpdate documentation for details on
|
35
|
+
# possible options.
|
36
|
+
def toggle_balloon_for(obj, value, options = {})
|
37
|
+
au = Kamelopard::AnimatedUpdate.new [], options
|
38
|
+
if ! obj.kind_of? Kamelopard::Placemark and ! obj.kind_of? Kamelopard::ScreenOverlay then
|
39
|
+
raise "Can't show balloons for things that aren't Placemarks or ScreenOverlays"
|
40
|
+
end
|
41
|
+
a = XML::Node.new 'Change'
|
42
|
+
# XXX This can probably be more robust, based on just the class's name
|
43
|
+
if obj.kind_of? Kamelopard::Placemark then
|
44
|
+
b = XML::Node.new 'Placemark'
|
45
|
+
else
|
46
|
+
b = XML::Node.new 'ScreenOverlay'
|
47
|
+
end
|
48
|
+
b.attributes['targetId'] = obj.kml_id
|
49
|
+
c = XML::Node.new 'gx:balloonVisibility'
|
50
|
+
c << XML::Node.new_text(value.to_s)
|
51
|
+
b << c
|
52
|
+
a << b
|
53
|
+
au << a
|
54
|
+
end
|
55
|
+
|
56
|
+
# Hides the popup balloon for a Placemark or ScreenOverlay object. Require
|
57
|
+
# the object as the first argument, and takes a hash of options passed to the
|
58
|
+
# AnimatedUpdate object this functino creates. See also show_balloon_for and
|
59
|
+
# toggle_balloon_for
|
60
|
+
def hide_balloon_for(obj, options = {})
|
61
|
+
toggle_balloon_for(obj, 0, options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Displays the popup balloon for a Placemark or ScreenOverlay object. Require
|
65
|
+
# the object as the first argument, and takes a hash of options passed to the
|
66
|
+
# AnimatedUpdate object this functino creates. See also show_balloon_for and
|
67
|
+
# toggle_balloon_for
|
68
|
+
def show_balloon_for(obj, options = {})
|
69
|
+
toggle_balloon_for(obj, 1, options)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Fades a placemark's popup balloon in or out. Takes as arguments the
|
73
|
+
# placemark object, 0 or 1 to hide or show the balloon, respectively, and a
|
74
|
+
# has of options to be passed to the AnimatedUpdate object created by this
|
75
|
+
# function. In order to have the balloon fade over some noticeable time, at
|
76
|
+
# minimum the :duration attribute in this hash should be set to some
|
77
|
+
# meaningful number of seconds.
|
78
|
+
def fade_balloon_for(obj, value, options = {})
|
79
|
+
au = Kamelopard::AnimatedUpdate.new [], options
|
80
|
+
if ! obj.is_a? Kamelopard::Placemark then
|
81
|
+
raise "Can't show balloons for things that aren't placemarks"
|
82
|
+
end
|
83
|
+
a = XML::Node.new 'Change'
|
84
|
+
b = XML::Node.new 'Placemark'
|
85
|
+
b.attributes['targetId'] = obj.kml_id
|
86
|
+
c = XML::Node.new 'color'
|
87
|
+
c << XML::Node.new_text(value.to_s)
|
88
|
+
b << c
|
89
|
+
a << b
|
90
|
+
au << a
|
91
|
+
end
|
92
|
+
|
93
|
+
# Refer to fade_balloon_for. This function only fades the balloon out.
|
94
|
+
def fade_out_balloon_for(obj, options = {})
|
95
|
+
fade_balloon_for(obj, '00ffffff', options)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Refer to fade_balloon_for. This function only fades the balloon in.
|
99
|
+
def fade_in_balloon_for(p, options = {})
|
100
|
+
fade_balloon_for(p, 'ffffffff', options)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Creates a Point object. Arguments are latitude, longitude, altitude,
|
104
|
+
# altitude mode, and extrude
|
105
|
+
def point(lo, la, alt=0, mode=nil, extrude = false)
|
106
|
+
m = ( mode.nil? ? :clampToGround : mode )
|
107
|
+
Kamelopard::Point.new(lo, la, alt, :altitudeMode => m, :extrude => extrude)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Creates a Placemark with the given name. Other Placemark attributes are set
|
111
|
+
# in the options hash. The note under the Kamelopard::Placemark class applies
|
112
|
+
# equally to the results of this function.
|
113
|
+
def placemark(name = nil, options = {})
|
114
|
+
Kamelopard::Placemark.new name, options
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the KML that makes up the current Kamelopard::Document
|
118
|
+
def get_kml
|
119
|
+
Kamelopard::DocumentHolder.instance.current_document.get_kml_document
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the KML that makes up the current Document, as a string
|
123
|
+
def get_kml_string
|
124
|
+
get_kml.to_s
|
125
|
+
end
|
126
|
+
|
127
|
+
# Inserts a KML gx:Wait element
|
128
|
+
def pause(p, options = {})
|
129
|
+
Kamelopard::Wait.new p, options
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the current Tour object
|
133
|
+
def get_tour()
|
134
|
+
Kamelopard::DocumentHolder.instance.current_document.tour
|
135
|
+
end
|
136
|
+
|
137
|
+
# Sets a name for the current Tour
|
138
|
+
def name_tour(name)
|
139
|
+
Kamelopard::DocumentHolder.instance.current_document.tour.name = name
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns the current Folder object
|
143
|
+
def get_folder()
|
144
|
+
f = Kamelopard::DocumentHolder.instance.current_document.folders.last
|
145
|
+
Kamelopard::Folder.new() if f.nil?
|
146
|
+
Kamelopard::DocumentHolder.instance.current_document.folders.last
|
147
|
+
end
|
148
|
+
|
149
|
+
# Creates a new Folder with the current name
|
150
|
+
def folder(name)
|
151
|
+
Kamelopard::Folder.new(name)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Names (or renames) the current Folder, and returns it
|
155
|
+
def name_folder(name)
|
156
|
+
Kamelopard::DocumentHolder.instance.current_document.folder.name = name
|
157
|
+
return Kamelopard::DocumentHolder.instance.current_document.folder
|
158
|
+
end
|
159
|
+
|
160
|
+
# Names (or renames) the current Document object, and returns it
|
161
|
+
def name_document(name)
|
162
|
+
Kamelopard::DocumentHolder.instance.current_document.name = name
|
163
|
+
return Kamelopard::DocumentHolder.instance.current_document
|
164
|
+
end
|
165
|
+
|
166
|
+
def zoom_out(dist = 1000, dur = 0, mode = nil)
|
167
|
+
l = Kamelopard::DocumentHolder.instance.current_document.tour.last_abs_view
|
168
|
+
raise "No current position to zoom out from\n" if l.nil?
|
169
|
+
l.range += dist
|
170
|
+
Kamelopard::FlyTo.new(l, nil, dur, mode)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Translates a heading into something between 0 and 360
|
174
|
+
def convert_heading(heading)
|
175
|
+
if heading > 360 then
|
176
|
+
step = -360
|
177
|
+
else
|
178
|
+
step = 360
|
179
|
+
end
|
180
|
+
while heading < 0 or heading > 360 do
|
181
|
+
heading = heading + step
|
182
|
+
end
|
183
|
+
heading
|
184
|
+
end
|
185
|
+
|
186
|
+
# Creates a list of FlyTo elements to orbit and look at a given point (center),
|
187
|
+
# at a given range (in meters), starting and ending at given angles (in
|
188
|
+
# degrees) from the center, where 0 and 360 (and -360, and 720, and -980, etc.)
|
189
|
+
# are north. To orbit multiple times, add or subtract 360 from the
|
190
|
+
# endHeading.
|
191
|
+
# The tilt argument matches the KML LookAt tilt argument
|
192
|
+
# already_there, if true, means we've already flown to the initial point
|
193
|
+
# The options hash can contain:
|
194
|
+
# :duration The total duration of the orbit. Defaults to 0, which means it will take 2 seconds per step
|
195
|
+
# :step How much to change the heading for each flyto. Defaults to some strange value >= 5
|
196
|
+
# :already_there
|
197
|
+
# Default false. Indicates that we've already flown to the initial position
|
198
|
+
def orbit(center, range = 100, tilt = 90, startHeading = 0, endHeading = 360, options = {})
|
199
|
+
duration = options.has_key?(:duration) ? options[:duration] : 0
|
200
|
+
step = options.has_key?(:step) ? options[:step] : nil
|
201
|
+
already_there = options.has_key?(:already_there) ? options[:already_there] : false
|
202
|
+
|
203
|
+
am = center.altitudeMode
|
204
|
+
|
205
|
+
if not step.nil? then
|
206
|
+
if (endHeading - startHeading > 0 and step < 0) or (endHeading - startHeading < 0 and step > 0) then
|
207
|
+
raise "Given start = #{startHeading}, end = #{endHeading}, and step = #{step}, this will be an infinite loop"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# We want at least 5 points (arbitrarily chosen value), plus at least 5 for
|
212
|
+
# each full revolution
|
213
|
+
|
214
|
+
# When I tried this all in one step, ruby told me 360 / 10 = 1805. I'm sure
|
215
|
+
# there's some reason why this is a feature and not a bug, but I'd rather
|
216
|
+
# not look it up right now.
|
217
|
+
dur = 2
|
218
|
+
if step.nil? then
|
219
|
+
num = (endHeading - startHeading).abs
|
220
|
+
num_steps = ((endHeading - startHeading) / 360.0).to_i.abs * 5 + 5
|
221
|
+
step = num / num_steps
|
222
|
+
step = 1 if step < 1
|
223
|
+
step = step * -1 if startHeading > endHeading
|
224
|
+
if already_there
|
225
|
+
num_steps = num_steps - 1
|
226
|
+
startHeading = startHeading + step
|
227
|
+
end
|
228
|
+
if duration != 0
|
229
|
+
dur = duration.to_f / num_steps
|
230
|
+
end
|
231
|
+
else
|
232
|
+
dur = duration * 1.0 / ((endHeading - startHeading) * 1.0 / step) if duration != 0
|
233
|
+
startHeading = startHeading + step if already_there
|
234
|
+
end
|
235
|
+
|
236
|
+
lastval = startHeading
|
237
|
+
mode = :bounce
|
238
|
+
mode = :smooth if already_there
|
239
|
+
startHeading.step(endHeading, step) do |theta|
|
240
|
+
lastval = theta
|
241
|
+
heading = convert_heading theta
|
242
|
+
fly_to Kamelopard::LookAt.new(center, :heading => heading, :tilt => tilt, :range => range, :altitudeMode => am), :duration => dur, :mode => mode
|
243
|
+
mode = :smooth
|
244
|
+
end
|
245
|
+
if lastval != endHeading then
|
246
|
+
fly_to Kamelopard::LookAt.new(center, :heading => convert_heading(endHeading), :tilt => tilt, :range => range, :altitudeMode => am), :duration => dur, :mode => :smooth
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Adds a SoundCue object.
|
251
|
+
def sound_cue(href, ds = nil)
|
252
|
+
Kamelopard::SoundCue.new href, ds
|
253
|
+
end
|
254
|
+
|
255
|
+
# XXX This implementation of orbit is trying to do things the hard way, but the code might be useful for other situations where the hard way is the only possible one
|
256
|
+
# def orbit(center, range = 100, startHeading = 0, endHeading = 360)
|
257
|
+
# p = ThreeDPointList.new()
|
258
|
+
#
|
259
|
+
# # Figure out how far we're going, and d
|
260
|
+
# dist = endHeading - startHeading
|
261
|
+
#
|
262
|
+
# # We want at least 5 points (arbitrarily chosen value), plus at least 5 for each full revolution
|
263
|
+
# step = (endHeading - startHeading) / ((endHeading - startHeading) / 360.0).to_i * 5 + 5
|
264
|
+
# startHeading.step(endHeading, step) do |theta|
|
265
|
+
# p << KMLPoint.new(
|
266
|
+
# center.longitude + Math.cos(theta),
|
267
|
+
# center.latitude + Math.sin(theta),
|
268
|
+
# center.altitude, center.altitudeMode)
|
269
|
+
# end
|
270
|
+
# p << KMLPoint.new(
|
271
|
+
# center.longitude + Math.cos(endHeading),
|
272
|
+
# center.latitude + Math.sin(endHeading),
|
273
|
+
# center.altitude, center.altitudeMode)
|
274
|
+
#
|
275
|
+
# p.interpolate.each do |a|
|
276
|
+
# fly_to
|
277
|
+
# end
|
278
|
+
# end
|
279
|
+
|
280
|
+
# Sets a prefix for all kml_id objects. Note that this does *not* change
|
281
|
+
# previously created objects' kml_ids... just new kml_ids going forward.
|
282
|
+
def set_prefix_to(a)
|
283
|
+
Kamelopard.id_prefix = a
|
284
|
+
end
|
285
|
+
|
286
|
+
# Writes KML output (and if applicable, viewsyncrelay configuration) to files.
|
287
|
+
# Include a file name for the actions_file argument to get viewsyncrelay
|
288
|
+
# configuration output as well. Note that this configuration includes only the
|
289
|
+
# actions section; users are responsible for creating appropriate linkages,
|
290
|
+
# inputs and outputs, and transformations, on their own, presumably in a
|
291
|
+
# separate file.
|
292
|
+
#
|
293
|
+
# If the filename ends in .kmz, it will create a KMZ file archive instead,
|
294
|
+
# with this file as doc.kml in that archive. Note that the KMZ will *not*
|
295
|
+
# contain any other files, like images that Overlay objects might depend on
|
296
|
+
#--
|
297
|
+
# TODO: Get it to add any other files it depends on (images, etc.) automatically
|
298
|
+
#++
|
299
|
+
def write_kml_to(filename = 'doc.kml', actions_file = 'actions.yml')
|
300
|
+
if filename =~ /\.kmz$/
|
301
|
+
# Require rubyzip here so that we don't write to doc.kml and then
|
302
|
+
# leave it there when we die later finding out we don't have
|
303
|
+
# rubyzip
|
304
|
+
require 'zip'
|
305
|
+
require 'tempfile'
|
306
|
+
|
307
|
+
kmlfile = Tempfile.new("kamelopard-#{filename}")
|
308
|
+
make_kmz = true
|
309
|
+
else
|
310
|
+
make_kmz = false
|
311
|
+
kmlfile = File.open(filename, 'w')
|
312
|
+
end
|
313
|
+
|
314
|
+
begin
|
315
|
+
kmlfile.write get_kml.to_s
|
316
|
+
if (get_document.vsr_actions.size > 0) then
|
317
|
+
File.open(actions_file, 'w') do |f| f.write get_document.get_actions end
|
318
|
+
end
|
319
|
+
|
320
|
+
if make_kmz
|
321
|
+
Zip::File.open(filename, Zip::File::CREATE) do |zipfile|
|
322
|
+
zipfile.add('doc.kml', kmlfile.path)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
ensure
|
326
|
+
kmlfile.close unless kmlfile.closed?
|
327
|
+
kmlfile.unlink if make_kmz
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Writes all KML documents in memory to their filenames, as set in their
|
332
|
+
# :filename properties. It's a good idea to set this property for each Document
|
333
|
+
# before calling this function.
|
334
|
+
def write_documents
|
335
|
+
get_doc_holder.documents.each do |d|
|
336
|
+
get_doc_holder.set_current d
|
337
|
+
write_kml_to d.filename
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Fades a screen overlay in or out. The show argument is boolean; true to
|
342
|
+
# show the overlay, or false to hide it. The fade will happen smoothly (as
|
343
|
+
# opposed to immediately) if the options hash includes a :duration element
|
344
|
+
# set to some positive number of seconds.
|
345
|
+
def fade_overlay(ov, show, options = {})
|
346
|
+
color = '00ffffff'
|
347
|
+
color = 'ffffffff' if show
|
348
|
+
if ov.is_a? String then
|
349
|
+
id = ov
|
350
|
+
else
|
351
|
+
id = ov.kml_id
|
352
|
+
end
|
353
|
+
|
354
|
+
a = XML::Node.new 'Change'
|
355
|
+
b = XML::Node.new 'ScreenOverlay'
|
356
|
+
b.attributes['targetId'] = id
|
357
|
+
c = XML::Node.new 'color'
|
358
|
+
c << XML::Node.new_text(color)
|
359
|
+
b << c
|
360
|
+
a << b
|
361
|
+
k = Kamelopard::AnimatedUpdate.new [a], options
|
362
|
+
end
|
363
|
+
|
364
|
+
# Given telemetry data, such as from an aircraft, including latitude,
|
365
|
+
# longitude, and altitude, this will figure out realistic-looking tilt,
|
366
|
+
# heading, and roll values and create a series of FlyTo objects to follow the
|
367
|
+
# extrapolated flight path
|
368
|
+
module TelemetryProcessor
|
369
|
+
def TelemetryProcessor.get_heading(p)
|
370
|
+
x1, y1, x2, y2 = [ p[1][0], p[1][1], p[2][0], p[2][1] ]
|
371
|
+
|
372
|
+
h = Math.atan((x2-x1) / (y2-y1)) * 180 / Math::PI
|
373
|
+
h = h + 180.0 if y2 < y1
|
374
|
+
h
|
375
|
+
end
|
376
|
+
|
377
|
+
def TelemetryProcessor.get_dist2(x1, y1, x2, y2)
|
378
|
+
Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2).abs
|
379
|
+
end
|
380
|
+
|
381
|
+
def TelemetryProcessor.get_dist3(x1, y1, z1, x2, y2, z2)
|
382
|
+
Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2 ).abs
|
383
|
+
end
|
384
|
+
|
385
|
+
def TelemetryProcessor.get_tilt(p)
|
386
|
+
x1, y1, z1, x2, y2, z2 = [ p[1][0], p[1][1], p[1][2], p[2][0], p[2][1], p[2][2] ]
|
387
|
+
smoothing_factor = 10.0
|
388
|
+
dist = get_dist3(x1, y1, z1, x2, y2, z2)
|
389
|
+
dist = dist + 1
|
390
|
+
# + 1 to avoid setting dist to 0, and having div-by-0 errors later
|
391
|
+
t = Math.atan((z2 - z1) / dist) * 180 / Math::PI / @@options[:exaggerate]
|
392
|
+
# the / 2.0 is just because it looked nicer that way
|
393
|
+
90.0 + t
|
394
|
+
end
|
395
|
+
|
396
|
+
def TelemetryProcessor.get_roll(p)
|
397
|
+
x1, y1, x2, y2, x3, y3 = [ p[0][0], p[0][1], p[1][0], p[1][1], p[2][0], p[2][1] ]
|
398
|
+
return 0 if x1.nil? or x2.nil?
|
399
|
+
|
400
|
+
# Measure roll based on angle between P1 -> P2 and P2 -> P3. To be really
|
401
|
+
# exact I ought to take into account altitude as well, but ... I don't want
|
402
|
+
# to
|
403
|
+
|
404
|
+
# Set x2, y2 as the origin
|
405
|
+
xn1 = x1 - x2
|
406
|
+
xn3 = x3 - x2
|
407
|
+
yn1 = y1 - y2
|
408
|
+
yn3 = y3 - y2
|
409
|
+
|
410
|
+
# Use dot product to get the angle between the two segments
|
411
|
+
angle = Math.acos( ((xn1 * xn3) + (yn1 * yn3)) / (get_dist2(0, 0, xn1, yn1).abs * get_dist2(0, 0, xn3, yn3).abs) ) * 180 / Math::PI
|
412
|
+
|
413
|
+
@@options[:exaggerate] * (angle - 180)
|
414
|
+
end
|
415
|
+
|
416
|
+
def TelemetryProcessor.fix_coord(a)
|
417
|
+
a = a - 360 if a > 180
|
418
|
+
a = a + 360 if a < -180
|
419
|
+
a
|
420
|
+
end
|
421
|
+
|
422
|
+
# This is the only function in the module that users are expected to
|
423
|
+
# call, and even then users should probably use the tour_from_points
|
424
|
+
# function. The p argument contains an ordered array of points, where
|
425
|
+
# each point is represented as an array consisting of longitude,
|
426
|
+
# latitude, and altitude, in that order. This will add a series of
|
427
|
+
# gx:FlyTo objects following the path determined by those points.
|
428
|
+
#--
|
429
|
+
# XXX Have some way to adjust FlyTo duration based on the distance
|
430
|
+
# between points, or based on user input.
|
431
|
+
#++
|
432
|
+
def TelemetryProcessor.add_flyto(p)
|
433
|
+
p2 = TelemetryProcessor::normalize_points p
|
434
|
+
p = p2
|
435
|
+
heading = get_heading p
|
436
|
+
tilt = get_tilt p
|
437
|
+
# roll = get_roll(last_last_lon, last_last_lat, last_lon, last_lat, lon, lat)
|
438
|
+
roll = get_roll p
|
439
|
+
#p = Kamelopard::Point.new last_lon, last_lat, last_alt, { :altitudeMode => :absolute }
|
440
|
+
point = Kamelopard::Point.new p[1][0], p[1][1], p[1][2], { :altitudeMode => :absolute }
|
441
|
+
c = Kamelopard::Camera.new point, { :heading => heading, :tilt => tilt, :roll => roll, :altitudeMode => :absolute }
|
442
|
+
f = Kamelopard::FlyTo.new c, { :duration => @@options[:pause], :mode => :smooth }
|
443
|
+
f.comment = "#{p[1][0]} #{p[1][1]} #{p[1][2]} to #{p[2][0]} #{p[2][1]} #{p[2][2]}"
|
444
|
+
end
|
445
|
+
|
446
|
+
def TelemetryProcessor.options=(a)
|
447
|
+
@@options = a
|
448
|
+
end
|
449
|
+
|
450
|
+
def TelemetryProcessor.normalize_points(p)
|
451
|
+
# The whole point here is to prevent problems when you cross the poles or the dateline
|
452
|
+
# This could have serious problems if points are really far apart, like
|
453
|
+
# hundreds of degrees. This seems unlikely.
|
454
|
+
lons = ((0..2).collect { |i| p[i][0] })
|
455
|
+
lats = ((0..2).collect { |i| p[i][1] })
|
456
|
+
|
457
|
+
lon_min, lon_max = lons.minmax
|
458
|
+
lat_min, lat_max = lats.minmax
|
459
|
+
|
460
|
+
if (lon_max - lon_min).abs > 200 then
|
461
|
+
(0..2).each do |i|
|
462
|
+
lons[i] += 360.0 if p[i][0] < 0
|
463
|
+
end
|
170
464
|
end
|
171
|
-
|
172
|
-
|
465
|
+
|
466
|
+
if (lat_max - lat_min).abs > 200 then
|
467
|
+
(0..2).each do |i|
|
468
|
+
lats[i] += 360.0 if p[i][1] < 0
|
469
|
+
end
|
173
470
|
end
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
471
|
+
|
472
|
+
return [
|
473
|
+
[ lons[0], lats[0], p[0][2] ],
|
474
|
+
[ lons[1], lats[1], p[1][2] ],
|
475
|
+
[ lons[2], lats[2], p[2][2] ],
|
476
|
+
]
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Creates a tour from a series of points, using TelemetryProcessor::add_flyto.
|
481
|
+
#
|
482
|
+
# The first argument is an ordered array of points, where each point is
|
483
|
+
# represented as an array of longitude, latitude, and altitude (in meters),
|
484
|
+
# in that order. The only options currently recognized are :pause and
|
485
|
+
# :exaggerate. :pause controls the flight speed by specifying the duration of
|
486
|
+
# each FlyTo element. Its default is 1 second. There is currently no
|
487
|
+
# mechanism for having anything other than constant durations between points.
|
488
|
+
# :exaggerate is an numeric value that defaults to 1; when set to larger
|
489
|
+
# values, it will exaggerate tilt and roll values, because they're sort of
|
490
|
+
# boring at normal scale.
|
491
|
+
def tour_from_points(points, options = {})
|
492
|
+
options.merge!({
|
493
|
+
:pause => 1,
|
494
|
+
:exaggerate => 1
|
495
|
+
}) { |key, old, new| old }
|
496
|
+
TelemetryProcessor.options = options
|
497
|
+
(0..(points.size-3)).each do |i|
|
498
|
+
TelemetryProcessor::add_flyto points[i,3]
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Given a hash of values, this creates an AbstractView object. Possible
|
503
|
+
# values in the hash are :latitude, :longitude, :altitude, :altitudeMode,
|
504
|
+
# :tilt, :heading, :roll, :range, :begin, :end, and :when. If the hash
|
505
|
+
# specifies :roll, a Camera object will result; otherwise, a LookAt object
|
506
|
+
# will result. Specifying both :roll and :range will still result in a Camera
|
507
|
+
# object, and the :range option will be ignored.
|
508
|
+
#
|
509
|
+
# :begin, :end, and :when are used to create the view's timestamp or timespan
|
510
|
+
#
|
511
|
+
# :roll, :range, and the timestamp / timespan options have no default; all
|
512
|
+
# other values default to 0 except :altitudeMode, which defaults to
|
513
|
+
# :relativeToGround.
|
514
|
+
def make_view_from(options = {})
|
515
|
+
o = {}
|
516
|
+
o.merge! options
|
517
|
+
options.each do |k, v|
|
518
|
+
o[k.to_sym] = v unless k.kind_of? Symbol
|
519
|
+
end
|
520
|
+
|
521
|
+
# Set defaults
|
522
|
+
[
|
523
|
+
[ :altitude, 0 ],
|
524
|
+
[ :altitudeMode, :relativeToGround ],
|
525
|
+
[ :latitude, 0 ],
|
526
|
+
[ :longitude, 0 ],
|
527
|
+
[ :tilt, 0 ],
|
528
|
+
[ :heading, 0 ],
|
529
|
+
[ :extrude, 0 ],
|
530
|
+
].each do |a|
|
531
|
+
o[a[0]] = a[1] unless o.has_key? a[0]
|
532
|
+
end
|
533
|
+
|
534
|
+
p = point o[:longitude].to_f, o[:latitude].to_f, o[:altitude].to_f, o[:altitudeMode], o[:extrude].to_i
|
535
|
+
|
536
|
+
if o.has_key? :roll then
|
537
|
+
view = Kamelopard::Camera.new p
|
538
|
+
else
|
539
|
+
view = Kamelopard::LookAt.new p
|
540
|
+
end
|
541
|
+
|
542
|
+
if o.has_key? :when then
|
543
|
+
o[:timestamp] = Kamelopard::TimeStamp.new(o[:when])
|
544
|
+
elsif o.has_key? :begin or o.has_key? :end then
|
545
|
+
(b, e) = [nil, nil]
|
546
|
+
b = o[:begin] if o.has_key? :begin
|
547
|
+
e = o[:end] if o.has_key? :end
|
548
|
+
o[:timespan] = Kamelopard::TimeSpan.new(b, e)
|
549
|
+
end
|
550
|
+
|
551
|
+
[ :altitudeMode, :timespan, :timestamp, :viewerOptions ].each do |a|
|
552
|
+
#p o[a] if o.has_key? a and a == :timestamp
|
553
|
+
view.method("#{a.to_s}=").call(o[a]) if o.has_key? a
|
554
|
+
end
|
555
|
+
|
556
|
+
[ :tilt, :heading, :range, :roll].each do |a|
|
557
|
+
#p o[a] if o.has_key? a and a == :timestamp
|
558
|
+
view.method("#{a.to_s}=").call(o[a].to_f) if o.has_key? a
|
559
|
+
end
|
560
|
+
|
561
|
+
view
|
562
|
+
end
|
563
|
+
|
564
|
+
# Creates a ScreenOverlay object
|
565
|
+
def screenoverlay(options = {})
|
566
|
+
Kamelopard::ScreenOverlay.new options
|
567
|
+
end
|
568
|
+
|
569
|
+
# Creates an XY object, for use when building Overlay objects
|
570
|
+
def xy(x = 0.5, y = 0.5, xt = :fraction, yt = :fraction)
|
571
|
+
Kamelopard::XY.new x, y, xt, yt
|
572
|
+
end
|
573
|
+
|
574
|
+
# Creates an IconStyle object.
|
575
|
+
def iconstyle(href = nil, options = {})
|
576
|
+
Kamelopard::IconStyle.new href, options
|
577
|
+
end
|
578
|
+
|
579
|
+
# Creates a LabelStyle object.
|
580
|
+
def labelstyle(scale = 1, options = {})
|
581
|
+
Kamelopard::LabelStyle.new scale, options
|
582
|
+
end
|
583
|
+
|
584
|
+
# Creates a BalloonStyle object.
|
585
|
+
def balloonstyle(text, options = {})
|
586
|
+
Kamelopard::BalloonStyle.new text, options
|
587
|
+
end
|
588
|
+
|
589
|
+
# Creates a Style object.
|
590
|
+
def style(options = {})
|
591
|
+
Kamelopard::Style.new options
|
592
|
+
end
|
593
|
+
|
594
|
+
# Creates a LookAt object focused on the given point
|
595
|
+
def look_at(point = nil, options = {})
|
596
|
+
Kamelopard::LookAt.new point, options
|
597
|
+
end
|
598
|
+
|
599
|
+
# Creates a Camera object focused on the given point
|
600
|
+
def camera(point = nil, options = {})
|
601
|
+
Kamelopard::Camera.new point, options
|
602
|
+
end
|
603
|
+
|
604
|
+
# Creates a FlyTo object flying to the given AbstractView
|
605
|
+
def fly_to(view = nil, options = {})
|
606
|
+
Kamelopard::FlyTo.new view, options
|
607
|
+
end
|
608
|
+
|
609
|
+
# Identical to each_placemark, only it deals in gx:FlyTo elements. Although it
|
610
|
+
# would be valid KML to use some prefix other than "gx" for Google's KML
|
611
|
+
# extension namespace, this function recognizes only "gx".
|
612
|
+
def each_flyto(d)
|
613
|
+
d.find('//gx:FlyTo').each do |p|
|
614
|
+
all_values = {}
|
615
|
+
|
616
|
+
# These fields are part of the abstractview
|
617
|
+
view_fields = %w{ latitude longitude heading range tilt roll altitude altitudeMode gx:altitudeMode coordinates}
|
618
|
+
# These are other field I'm interested in
|
619
|
+
other_fields = %w{ description name }
|
620
|
+
all_fields = view_fields.clone
|
621
|
+
all_fields.concat(other_fields.clone)
|
622
|
+
all_fields.each do |k|
|
623
|
+
if k == 'gx:altitudeMode' then
|
624
|
+
ix = k
|
625
|
+
next unless p.find_first('kml:altitudeMode').nil?
|
626
|
+
else
|
627
|
+
ix = "kml:#{k}"
|
199
628
|
end
|
629
|
+
r = k == "gx:altitudeMode" ? :altitudeMode : k.to_sym
|
630
|
+
tmp = p.find_first("descendant::#{ix}")
|
631
|
+
next if tmp.nil?
|
632
|
+
all_values[k == "gx:altitudeMode" ? :altitudeMode : k.to_sym ] = tmp.content
|
200
633
|
end
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
634
|
+
view_values = {}
|
635
|
+
view_fields.each do |v| view_values[v.to_sym] = all_values[v.to_sym].clone if all_values.has_key? v.to_sym end
|
636
|
+
yield make_view_from(view_values), all_values
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
# Pulls all Placemark elements from the XML::Document d and yields each in turn
|
641
|
+
# to the caller. Assumes "kml" is the namespace prefix for standard KML objects.
|
642
|
+
#--
|
643
|
+
# XXX This currently expects Placemarks to contain LookAt or Camera objects. It
|
644
|
+
# should do something useful with placemarks based on Points, or other objects
|
645
|
+
#++
|
646
|
+
def each_placemark(d)
|
647
|
+
d.find('//kml:Placemark').each do |p|
|
648
|
+
all_values = {}
|
649
|
+
|
650
|
+
# These fields are part of the abstractview
|
651
|
+
view_fields = %w{ latitude longitude heading range tilt roll altitude altitudeMode gx:altitudeMode }
|
652
|
+
# These are other field I'm interested in
|
653
|
+
other_fields = %w{ description name }
|
654
|
+
all_fields = view_fields.clone
|
655
|
+
all_fields.concat(other_fields.clone)
|
656
|
+
all_fields.each do |k|
|
657
|
+
if k == 'gx:altitudeMode' then
|
658
|
+
ix = k
|
659
|
+
next unless p.find_first('kml:altitudeMode').nil?
|
660
|
+
else
|
661
|
+
ix = "kml:#{k}"
|
662
|
+
end
|
663
|
+
r = k == "gx:altitudeMode" ? :altitudeMode : k.to_sym
|
664
|
+
tmp = p.find_first("descendant::#{ix}")
|
665
|
+
next if tmp.nil?
|
666
|
+
all_values[k == "gx:altitudeMode" ? :altitudeMode : k.to_sym ] = tmp.content
|
225
667
|
end
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
668
|
+
view_values = {}
|
669
|
+
view_fields.each do |v| view_values[v.to_sym] = all_values[v.to_sym].clone if all_values.has_key? v.to_sym end
|
670
|
+
yield make_view_from(view_values), all_values
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
674
|
+
# Makes an HTML tour index, linked to a one-pixel screen overlay. The HTML
|
675
|
+
# contains links to start each tour.
|
676
|
+
def make_tour_index(erb = nil, options = {})
|
677
|
+
get_document.make_tour_index(erb, options)
|
678
|
+
end
|
679
|
+
|
680
|
+
# Superceded by toggle_balloon_for, but retained for backward compatibility
|
681
|
+
def show_hide_balloon(p, wait, options = {})
|
682
|
+
show_balloon_for p, options
|
683
|
+
pause wait
|
684
|
+
hide_balloon_for p, options
|
685
|
+
end
|
686
|
+
|
687
|
+
# Creates a CDATA XML::Node. This is useful for, among other things,
|
688
|
+
# ExtendedData values
|
689
|
+
def cdata(text)
|
690
|
+
XML::Node.new_cdata text.to_s
|
691
|
+
end
|
692
|
+
|
693
|
+
# Returns an array of two values, equal to l +/- p%, defining a "band" around the central value l
|
694
|
+
# NB! p is interpreted as a percentage, not a fraction. IOW the result is divided by 100.0.
|
695
|
+
def band(l, p)
|
696
|
+
f = l * p / 100.0
|
697
|
+
[ l - f, l + f ]
|
698
|
+
end
|
699
|
+
|
700
|
+
# Ensures v is within the range [min, max]. Modifies v to be within that range,
|
701
|
+
# assuming the number line is circular (as with latitude or longitude)
|
702
|
+
def circ_bounds(v, max, min)
|
703
|
+
w = max - min
|
704
|
+
if v > max then
|
705
|
+
while (v > max) do
|
706
|
+
v = v - w
|
235
707
|
end
|
236
|
-
|
237
|
-
|
708
|
+
elsif v < min then
|
709
|
+
while (v < min) do
|
710
|
+
v = v + w
|
238
711
|
end
|
239
712
|
end
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
# center.longitude + Math.cos(theta),
|
258
|
-
# center.latitude + Math.sin(theta),
|
259
|
-
# center.altitude, center.altitudeMode)
|
260
|
-
# end
|
261
|
-
# p << KMLPoint.new(
|
262
|
-
# center.longitude + Math.cos(endHeading),
|
263
|
-
# center.latitude + Math.sin(endHeading),
|
264
|
-
# center.altitude, center.altitudeMode)
|
265
|
-
#
|
266
|
-
# p.interpolate.each do |a|
|
267
|
-
# fly_to
|
268
|
-
# end
|
269
|
-
# end
|
270
|
-
|
271
|
-
# Sets a prefix for all kml_id objects. Note that this does *not* change
|
272
|
-
# previously created objects' kml_ids... just new kml_ids going forward.
|
273
|
-
def set_prefix_to(a)
|
274
|
-
Kamelopard.id_prefix = a
|
275
|
-
end
|
276
|
-
|
277
|
-
# Writes KML output (and if applicable, viewsyncrelay configuration) to files.
|
278
|
-
# Include a file name for the actions_file argument to get viewsyncrelay
|
279
|
-
# configuration output as well. Note that this configuration includes only the
|
280
|
-
# actions section; users are responsible for creating appropriate linkages,
|
281
|
-
# inputs and outputs, and transformations, on their own, presumably in a
|
282
|
-
# separate file.
|
283
|
-
def write_kml_to(file = 'doc.kml', actions_file = 'actions.yml')
|
284
|
-
File.open(file, 'w') do |f| f.write get_kml.to_s end
|
285
|
-
if (get_document.vsr_actions.size > 0) then
|
286
|
-
File.open(actions_file, 'w') do |f| f.write get_document.get_actions end
|
287
|
-
end
|
288
|
-
#File.open(file, 'w') do |f| f.write get_kml.to_s.gsub(/balloonVis/, 'gx:balloonVis') end
|
289
|
-
end
|
290
|
-
|
291
|
-
# Fades a screen overlay in or out. The show argument is boolean; true to
|
292
|
-
# show the overlay, or false to hide it. The fade will happen smoothly (as
|
293
|
-
# opposed to immediately) if the options hash includes a :duration element
|
294
|
-
# set to some positive number of seconds.
|
295
|
-
def fade_overlay(ov, show, options = {})
|
296
|
-
color = '00ffffff'
|
297
|
-
color = 'ffffffff' if show
|
298
|
-
if ov.is_a? String then
|
299
|
-
id = ov
|
300
|
-
else
|
301
|
-
id = ov.kml_id
|
302
|
-
end
|
303
|
-
|
304
|
-
a = XML::Node.new 'Change'
|
305
|
-
b = XML::Node.new 'ScreenOverlay'
|
306
|
-
b.attributes['targetId'] = id
|
307
|
-
c = XML::Node.new 'color'
|
308
|
-
c << XML::Node.new_text(color)
|
309
|
-
b << c
|
310
|
-
a << b
|
311
|
-
k = Kamelopard::AnimatedUpdate.new [a], options
|
312
|
-
end
|
313
|
-
|
314
|
-
# Given telemetry data, such as from an aircraft, including latitude,
|
315
|
-
# longitude, and altitude, this will figure out realistic-looking tilt,
|
316
|
-
# heading, and roll values and create a series of FlyTo objects to follow the
|
317
|
-
# extrapolated flight path
|
318
|
-
module TelemetryProcessor
|
319
|
-
Pi = 3.1415926535
|
320
|
-
|
321
|
-
def TelemetryProcessor.get_heading(p)
|
322
|
-
x1, y1, x2, y2 = [ p[1][0], p[1][1], p[2][0], p[2][1] ]
|
323
|
-
|
324
|
-
h = Math.atan((x2-x1) / (y2-y1)) * 180 / Pi
|
325
|
-
h = h + 180.0 if y2 < y1
|
326
|
-
h
|
327
|
-
end
|
328
|
-
|
329
|
-
def TelemetryProcessor.get_dist2(x1, y1, x2, y2)
|
330
|
-
Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2).abs
|
331
|
-
end
|
332
|
-
|
333
|
-
def TelemetryProcessor.get_dist3(x1, y1, z1, x2, y2, z2)
|
334
|
-
Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2 ).abs
|
335
|
-
end
|
336
|
-
|
337
|
-
def TelemetryProcessor.get_tilt(p)
|
338
|
-
x1, y1, z1, x2, y2, z2 = [ p[1][0], p[1][1], p[1][2], p[2][0], p[2][1], p[2][2] ]
|
339
|
-
smoothing_factor = 10.0
|
340
|
-
dist = get_dist3(x1, y1, z1, x2, y2, z2)
|
341
|
-
dist = dist + 1
|
342
|
-
# + 1 to avoid setting dist to 0, and having div-by-0 errors later
|
343
|
-
t = Math.atan((z2 - z1) / dist) * 180 / Pi / @@options[:exaggerate]
|
344
|
-
# the / 2.0 is just because it looked nicer that way
|
345
|
-
90.0 + t
|
346
|
-
end
|
347
|
-
|
348
|
-
def TelemetryProcessor.get_roll(p)
|
349
|
-
x1, y1, x2, y2, x3, y3 = [ p[0][0], p[0][1], p[1][0], p[1][1], p[2][0], p[2][1] ]
|
350
|
-
return 0 if x1.nil? or x2.nil?
|
351
|
-
|
352
|
-
# Measure roll based on angle between P1 -> P2 and P2 -> P3. To be really
|
353
|
-
# exact I ought to take into account altitude as well, but ... I don't want
|
354
|
-
# to
|
355
|
-
|
356
|
-
# Set x2, y2 as the origin
|
357
|
-
xn1 = x1 - x2
|
358
|
-
xn3 = x3 - x2
|
359
|
-
yn1 = y1 - y2
|
360
|
-
yn3 = y3 - y2
|
361
|
-
|
362
|
-
# Use dot product to get the angle between the two segments
|
363
|
-
angle = Math.acos( ((xn1 * xn3) + (yn1 * yn3)) / (get_dist2(0, 0, xn1, yn1).abs * get_dist2(0, 0, xn3, yn3).abs) ) * 180 / Pi
|
364
|
-
|
365
|
-
@@options[:exaggerate] * (angle - 180)
|
366
|
-
end
|
367
|
-
|
368
|
-
def TelemetryProcessor.fix_coord(a)
|
369
|
-
a = a - 360 if a > 180
|
370
|
-
a = a + 360 if a < -180
|
371
|
-
a
|
372
|
-
end
|
373
|
-
|
374
|
-
# This is the only function in the module that users are expected to
|
375
|
-
# call, and even then users should probably use the tour_from_points
|
376
|
-
# function. The p argument contains an ordered array of points, where
|
377
|
-
# each point is represented as an array consisting of longitude,
|
378
|
-
# latitude, and altitude, in that order. This will add a series of
|
379
|
-
# gx:FlyTo objects following the path determined by those points.
|
380
|
-
#--
|
381
|
-
# XXX Have some way to adjust FlyTo duration based on the distance
|
382
|
-
# between points, or based on user input.
|
383
|
-
#++
|
384
|
-
def TelemetryProcessor.add_flyto(p)
|
385
|
-
p2 = TelemetryProcessor::normalize_points p
|
386
|
-
p = p2
|
387
|
-
heading = get_heading p
|
388
|
-
tilt = get_tilt p
|
389
|
-
# roll = get_roll(last_last_lon, last_last_lat, last_lon, last_lat, lon, lat)
|
390
|
-
roll = get_roll p
|
391
|
-
#p = Kamelopard::Point.new last_lon, last_lat, last_alt, { :altitudeMode => :absolute }
|
392
|
-
point = Kamelopard::Point.new p[1][0], p[1][1], p[1][2], { :altitudeMode => :absolute }
|
393
|
-
c = Kamelopard::Camera.new point, { :heading => heading, :tilt => tilt, :roll => roll, :altitudeMode => :absolute }
|
394
|
-
f = Kamelopard::FlyTo.new c, { :duration => @@options[:pause], :mode => :smooth }
|
395
|
-
f.comment = "#{p[1][0]} #{p[1][1]} #{p[1][2]} to #{p[2][0]} #{p[2][1]} #{p[2][2]}"
|
396
|
-
end
|
397
|
-
|
398
|
-
def TelemetryProcessor.options=(a)
|
399
|
-
@@options = a
|
400
|
-
end
|
401
|
-
|
402
|
-
def TelemetryProcessor.normalize_points(p)
|
403
|
-
# The whole point here is to prevent problems when you cross the poles or the dateline
|
404
|
-
# This could have serious problems if points are really far apart, like
|
405
|
-
# hundreds of degrees. This seems unlikely.
|
406
|
-
lons = ((0..2).collect { |i| p[i][0] })
|
407
|
-
lats = ((0..2).collect { |i| p[i][1] })
|
408
|
-
|
409
|
-
lon_min, lon_max = lons.minmax
|
410
|
-
lat_min, lat_max = lats.minmax
|
411
|
-
|
412
|
-
if (lon_max - lon_min).abs > 200 then
|
413
|
-
(0..2).each do |i|
|
414
|
-
lons[i] += 360.0 if p[i][0] < 0
|
415
|
-
end
|
416
|
-
end
|
417
|
-
|
418
|
-
if (lat_max - lat_min).abs > 200 then
|
419
|
-
(0..2).each do |i|
|
420
|
-
lats[i] += 360.0 if p[i][1] < 0
|
421
|
-
end
|
422
|
-
end
|
423
|
-
|
424
|
-
return [
|
425
|
-
[ lons[0], lats[0], p[0][2] ],
|
426
|
-
[ lons[1], lats[1], p[1][2] ],
|
427
|
-
[ lons[2], lats[2], p[2][2] ],
|
428
|
-
]
|
429
|
-
end
|
430
|
-
end
|
431
|
-
|
432
|
-
# Creates a tour from a series of points, using TelemetryProcessor::add_flyto.
|
433
|
-
#
|
434
|
-
# The first argument is an ordered array of points, where each point is
|
435
|
-
# represented as an array of longitude, latitude, and altitude (in meters),
|
436
|
-
# in that order. The only options currently recognized are :pause and
|
437
|
-
# :exaggerate. :pause controls the flight speed by specifying the duration of
|
438
|
-
# each FlyTo element. Its default is 1 second. There is currently no
|
439
|
-
# mechanism for having anything other than constant durations between points.
|
440
|
-
# :exaggerate is an numeric value that defaults to 1; when set to larger
|
441
|
-
# values, it will exaggerate tilt and roll values, because they're sort of
|
442
|
-
# boring at normal scale.
|
443
|
-
def tour_from_points(points, options = {})
|
444
|
-
options.merge!({
|
445
|
-
:pause => 1,
|
446
|
-
:exaggerate => 1
|
447
|
-
}) { |key, old, new| old }
|
448
|
-
TelemetryProcessor.options = options
|
449
|
-
(0..(points.size-3)).each do |i|
|
450
|
-
TelemetryProcessor::add_flyto points[i,3]
|
451
|
-
end
|
452
|
-
end
|
453
|
-
|
454
|
-
# Given a hash of values, this creates an AbstractView object. Possible
|
455
|
-
# values in the hash are :latitude, :longitude, :altitude, :altitudeMode,
|
456
|
-
# :tilt, :heading, :roll, :range, :begin, :end, and :when. If the hash
|
457
|
-
# specifies :roll, a Camera object will result; otherwise, a LookAt object
|
458
|
-
# will result. Specifying both :roll and :range will still result in a Camera
|
459
|
-
# object, and the :range option will be ignored.
|
460
|
-
#
|
461
|
-
# :begin, :end, and :when are used to create the view's timestamp or timespan
|
462
|
-
#
|
463
|
-
# :roll, :range, and the timestamp / timespan options have no default; all
|
464
|
-
# other values default to 0 except :altitudeMode, which defaults to
|
465
|
-
# :relativeToGround.
|
466
|
-
def make_view_from(options = {})
|
467
|
-
o = {}
|
468
|
-
o.merge! options
|
469
|
-
options.each do |k, v|
|
470
|
-
o[k.to_sym] = v unless k.kind_of? Symbol
|
471
|
-
end
|
472
|
-
|
473
|
-
# Set defaults
|
474
|
-
[
|
475
|
-
[ :altitude, 0 ],
|
476
|
-
[ :altitudeMode, :relativeToGround ],
|
477
|
-
[ :latitude, 0 ],
|
478
|
-
[ :longitude, 0 ],
|
479
|
-
[ :tilt, 0 ],
|
480
|
-
[ :heading, 0 ],
|
481
|
-
[ :extrude, 0 ],
|
482
|
-
].each do |a|
|
483
|
-
o[a[0]] = a[1] unless o.has_key? a[0]
|
484
|
-
end
|
485
|
-
|
486
|
-
p = point o[:longitude], o[:latitude], o[:altitude], o[:altitudeMode], o[:extrude]
|
487
|
-
|
488
|
-
if o.has_key? :roll then
|
489
|
-
view = Kamelopard::Camera.new p
|
490
|
-
else
|
491
|
-
view = Kamelopard::LookAt.new p
|
492
|
-
end
|
493
|
-
|
494
|
-
if o.has_key? :when then
|
495
|
-
o[:timestamp] = Kamelopard::TimeStamp.new(o[:when])
|
496
|
-
elsif o.has_key? :begin or o.has_key? :end then
|
497
|
-
(b, e) = [nil, nil]
|
498
|
-
b = o[:begin] if o.has_key? :begin
|
499
|
-
e = o[:end] if o.has_key? :end
|
500
|
-
o[:timespan] = Kamelopard::TimeSpan.new(b, e)
|
501
|
-
end
|
502
|
-
|
503
|
-
[ :altitudeMode, :tilt, :heading, :timespan, :timestamp, :range, :roll, :viewerOptions ].each do |a|
|
504
|
-
#p o[a] if o.has_key? a and a == :timestamp
|
505
|
-
view.method("#{a.to_s}=").call(o[a]) if o.has_key? a
|
506
|
-
end
|
507
|
-
|
508
|
-
view
|
509
|
-
end
|
510
|
-
|
511
|
-
# Creates a ScreenOverlay object
|
512
|
-
def screenoverlay(options = {})
|
513
|
-
Kamelopard::ScreenOverlay.new options
|
514
|
-
end
|
515
|
-
|
516
|
-
# Creates an XY object, for use when building Overlay objects
|
517
|
-
def xy(x = 0.5, y = 0.5, xt = :fraction, yt = :fraction)
|
518
|
-
Kamelopard::XY.new x, y, xt, yt
|
519
|
-
end
|
520
|
-
|
521
|
-
# Creates an IconStyle object.
|
522
|
-
def iconstyle(href = nil, options = {})
|
523
|
-
Kamelopard::IconStyle.new href, options
|
524
|
-
end
|
525
|
-
|
526
|
-
# Creates an LabelStyle object.
|
527
|
-
def labelstyle(scale = 1, options = {})
|
528
|
-
Kamelopard::LabelStyle.new scale, options
|
529
|
-
end
|
530
|
-
|
531
|
-
# Creates an BalloonStyle object.
|
532
|
-
def balloonstyle(text, options = {})
|
533
|
-
Kamelopard::BalloonStyle.new text, options
|
534
|
-
end
|
535
|
-
|
536
|
-
# Creates an Style object.
|
537
|
-
def style(options = {})
|
538
|
-
Kamelopard::Style.new options
|
539
|
-
end
|
540
|
-
|
541
|
-
# Creates a LookAt object focused on the given point
|
542
|
-
def look_at(point = nil, options = {})
|
543
|
-
Kamelopard::LookAt.new point, options
|
544
|
-
end
|
545
|
-
|
546
|
-
# Creates a Camera object focused on the given point
|
547
|
-
def camera(point = nil, options = {})
|
548
|
-
Kamelopard::Camera.new point, options
|
549
|
-
end
|
550
|
-
|
551
|
-
# Creates a FlyTo object flying to the given AbstractView
|
552
|
-
def fly_to(view = nil, options = {})
|
553
|
-
Kamelopard::FlyTo.new view, options
|
554
|
-
end
|
555
|
-
|
556
|
-
# Pulls the Placemarks from the KML document d and yields each in turn to the caller
|
557
|
-
# d = an XML::Document containing KML
|
558
|
-
# XXX This currently expects Placemarks to contain LookAt or Camera objects. It should do something useful with placemarks based on Points, or other objects
|
559
|
-
def each_placemark(d)
|
560
|
-
d.find('//kml:Placemark').each do |p|
|
561
|
-
all_values = {}
|
562
|
-
|
563
|
-
# These fields are part of the abstractview
|
564
|
-
view_fields = %w{ latitude longitude heading range tilt roll altitude altitudeMode gx:altitudeMode }
|
565
|
-
# These are other field I'm interested in
|
566
|
-
other_fields = %w{ description name }
|
567
|
-
all_fields = view_fields.clone
|
568
|
-
all_fields.concat(other_fields.clone)
|
569
|
-
all_fields.each do |k|
|
570
|
-
if k == 'gx:altitudeMode' then
|
571
|
-
ix = k
|
572
|
-
next unless p.find_first('kml:altitudeMode').nil?
|
573
|
-
else
|
574
|
-
ix = "kml:#{k}"
|
575
|
-
end
|
576
|
-
r = k == "gx:altitudeMode" ? :altitudeMode : k.to_sym
|
577
|
-
tmp = p.find_first("descendant::#{ix}")
|
578
|
-
next if tmp.nil?
|
579
|
-
all_values[k == "gx:altitudeMode" ? :altitudeMode : k.to_sym ] = tmp.content
|
580
|
-
end
|
581
|
-
view_values = {}
|
582
|
-
view_fields.each do |v| view_values[v.to_sym] = all_values[v.to_sym].clone if all_values.has_key? v.to_sym end
|
583
|
-
yield make_view_from(view_values), all_values
|
584
|
-
end
|
585
|
-
end
|
586
|
-
|
587
|
-
# Makes an HTML tour index, linked to a one-pixel screen overlay. The HTML
|
588
|
-
# contains links to start each tour.
|
589
|
-
def make_tour_index(erb = nil, options = {})
|
590
|
-
get_document.make_tour_index(erb, options)
|
591
|
-
end
|
592
|
-
|
593
|
-
# Superceded by toggle_balloon_for, but retained for backward compatibility
|
594
|
-
def show_hide_balloon(p, wait, options = {})
|
595
|
-
show_balloon_for p, options
|
596
|
-
pause wait
|
597
|
-
hide_balloon_for p, options
|
598
|
-
end
|
599
|
-
|
600
|
-
# Creates a CDATA XML::Node. This is useful for, among other things,
|
601
|
-
# ExtendedData values
|
602
|
-
def cdata(text)
|
603
|
-
XML::Node.new_cdata text.to_s
|
604
|
-
end
|
605
|
-
|
606
|
-
# Returns an array of two values, equal to l +/- p%, defining a "band" around the central value l
|
607
|
-
# NB! p is interpreted as a percentage, not a fraction. IOW the result is divided by 100.0.
|
608
|
-
def band(l, p)
|
609
|
-
f = l * p / 100.0
|
610
|
-
[ l - f, l + f ]
|
611
|
-
end
|
612
|
-
|
613
|
-
|
614
|
-
# Ensures v is within the range [min, max]. Modifies v to be within that range,
|
615
|
-
# assuming the number line is circular (as with latitude or longitude)
|
616
|
-
def circ_bounds(v, max, min)
|
617
|
-
w = max - min
|
618
|
-
if v > max then
|
619
|
-
while (v > max) do
|
620
|
-
v = v - w
|
621
|
-
end
|
622
|
-
elsif v < min then
|
623
|
-
while (v < min) do
|
624
|
-
v = v + w
|
625
|
-
end
|
626
|
-
end
|
627
|
-
v
|
628
|
-
end
|
629
|
-
|
630
|
-
# These functions ensure the given value is within appropriate bounds for a
|
631
|
-
# latitude or longitude. Modifies it as necessary if it's not.
|
632
|
-
def lat_check(l)
|
633
|
-
circ_bounds(l * 1.0, 90.0, -90.0)
|
634
|
-
end
|
635
|
-
|
636
|
-
# See lat_check()
|
637
|
-
def long_check(l)
|
638
|
-
circ_bounds(l * 1.0, 180.0, -180.0)
|
639
|
-
end
|
640
|
-
|
641
|
-
# Turns an array of two values (min, max) into a string suitable for use as a
|
642
|
-
# viewsyncrelay constraint
|
643
|
-
def to_constraint(arr)
|
713
|
+
v
|
714
|
+
end
|
715
|
+
|
716
|
+
# These functions ensure the given value is within appropriate bounds for a
|
717
|
+
# latitude or longitude. Modifies it as necessary if it's not.
|
718
|
+
def lat_check(l)
|
719
|
+
circ_bounds(l * 1.0, 90.0, -90.0)
|
720
|
+
end
|
721
|
+
|
722
|
+
# See lat_check()
|
723
|
+
def long_check(l)
|
724
|
+
circ_bounds(l * 1.0, 180.0, -180.0)
|
725
|
+
end
|
726
|
+
|
727
|
+
# Turns an array of two values (min, max) into a string suitable for use as a
|
728
|
+
# viewsyncrelay constraint
|
729
|
+
def to_constraint(arr)
|
644
730
|
"[#{arr[0]}, #{arr[1]}]"
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
# XXX Finish this
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
731
|
+
end
|
732
|
+
|
733
|
+
# Adds a VSRAction object (a viewsyncrelay action) to the document, for
|
734
|
+
# viewsyncrelay configuration
|
735
|
+
#def do_action(cmd, options = {})
|
736
|
+
# # XXX Finish this
|
737
|
+
#end
|
738
|
+
|
739
|
+
# Returns the Document's VSRActions as a YAML string, suitable for writing to
|
740
|
+
# a viewsyncrelay configuration file
|
741
|
+
def get_actions
|
656
742
|
get_document.get_actions_yaml
|
657
|
-
|
743
|
+
end
|
658
744
|
|
659
|
-
|
660
|
-
|
745
|
+
# Writes actions to a viewsyncrelay config file
|
746
|
+
def write_actions_to(filename = 'actions.yml')
|
661
747
|
File.open(filename, 'w') do |f| f.write get_actions end
|
662
|
-
|
748
|
+
end
|
663
749
|
|
664
|
-
|
665
|
-
|
666
|
-
|
750
|
+
# Generates a series of points in a path that will simulate Earth's FlyTo in
|
751
|
+
# bounce mode, from one view to another. Note that the view objects must be
|
752
|
+
# the same type: either LookAt, or Camera. Options include :no_flyto and
|
753
|
+
# :show_placemarks, and match make_function_path's meanings for those
|
754
|
+
# options
|
755
|
+
#--
|
756
|
+
# XXX Fix the limitation that the views must be the same type
|
757
|
+
# XXX Make it slow down a bit toward the end of the run
|
758
|
+
#++
|
759
|
+
def bounce(a, b, duration, points, options = {})
|
760
|
+
raise "Arguments to bounce() must either be Camera or LookAt objects, and must be the same type" unless
|
761
|
+
((a.kind_of? Kamelopard::Camera and b.kind_of? Kamelopard::Camera) or
|
762
|
+
(a.kind_of? Kamelopard::LookAt and b.kind_of? Kamelopard::LookAt))
|
763
|
+
# The idea here is just to generate a function; the hard bit is finding
|
764
|
+
# control points.
|
765
|
+
include Kamelopard
|
766
|
+
include Kamelopard::Functions
|
667
767
|
|
668
|
-
|
669
|
-
|
670
|
-
# the same time: either LookAt, or Camera. Options include :no_flyto and
|
671
|
-
# :show_placemarks, and match make_function_path's meanings for those
|
672
|
-
# options
|
673
|
-
#--
|
674
|
-
# XXX Fix the limitation that the views must be the same type
|
675
|
-
# XXX Make it slow down a bit toward the end of the run
|
676
|
-
#++
|
677
|
-
def bounce(a, b, duration, points, options = {})
|
678
|
-
raise "Arguments to bounce() must either be Camera or LookAt objects, and must be the same type" unless
|
679
|
-
((a.kind_of? Kamelopard::Camera and b.kind_of? Kamelopard::Camera) or
|
680
|
-
(a.kind_of? Kamelopard::LookAt and b.kind_of? Kamelopard::LookAt))
|
681
|
-
# The idea here is just to generate a function; the hard bit is finding
|
682
|
-
# control points.
|
683
|
-
include Kamelopard
|
684
|
-
include Kamelopard::Functions
|
685
|
-
|
686
|
-
max_alt = a.altitude
|
687
|
-
max_alt = b.altitude if b.altitude > max_alt
|
688
|
-
|
689
|
-
bounce_alt = 1.3 * (b.altitude - a.altitude).abs
|
690
|
-
# 150 is the result of trial-and-error
|
691
|
-
gc = 0.8 * great_circle_distance(a, b) * 150
|
692
|
-
bounce_alt = gc if gc > bounce_alt
|
693
|
-
#raise "wtf: #{a.inspect}, #{b.inspect}"
|
694
|
-
|
695
|
-
latlonfunc = LatLonInterp.new(a, b) do |x, y, z|
|
696
|
-
Line.interpolate(x, y)
|
697
|
-
end
|
768
|
+
max_alt = a.altitude
|
769
|
+
max_alt = b.altitude if b.altitude > max_alt
|
698
770
|
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
# way for us to figure out absolute altitudes given, say,
|
708
|
-
# :relativeToGround
|
709
|
-
# ymin, ymax x1 y1
|
710
|
-
:altitude => Quadratic.interpolate(a.altitude, b.altitude, 0.5, bounce_alt),
|
711
|
-
:altitudeMode => a.altitudeMode,
|
712
|
-
:duration => duration * 1.0 / points,
|
713
|
-
}
|
714
|
-
opts[:no_flyto] = 1 if options.has_key?(:no_flyto)
|
715
|
-
opts[:show_placemarks] = 1 if options.has_key?(:show_placemarks)
|
716
|
-
|
717
|
-
if a.kind_of? Camera then
|
718
|
-
opts[:roll] = Line.interpolate(a.roll, b.roll)
|
719
|
-
else
|
720
|
-
opts[:range] = Line.interpolate(a.range, b.range)
|
721
|
-
end
|
722
|
-
return make_function_path(points, opts)
|
771
|
+
bounce_alt = 1.3 * (b.altitude - a.altitude).abs
|
772
|
+
# 150 is the result of trial-and-error
|
773
|
+
gc = 0.8 * great_circle_distance(a, b) * 150
|
774
|
+
bounce_alt = gc if gc > bounce_alt
|
775
|
+
#raise "wtf: #{a.inspect}, #{b.inspect}"
|
776
|
+
|
777
|
+
latlonfunc = LatLonInterp.new(a, b) do |x, y, z|
|
778
|
+
Line.interpolate(x, y)
|
723
779
|
end
|
724
780
|
|
725
|
-
|
726
|
-
|
727
|
-
|
781
|
+
opts = {
|
782
|
+
:latitude => Line.interpolate(a.latitude, b.latitude),
|
783
|
+
:longitude => Line.interpolate(a.longitude, b.longitude),
|
784
|
+
:multidim => [[ latlonfunc, [ :latitude, :longitude ]]],
|
785
|
+
:heading => Line.interpolate(a.heading, b.heading),
|
786
|
+
:tilt => Line.interpolate(a.tilt, b.tilt),
|
787
|
+
# XXX This doesn't really work. An actual altitude requires a
|
788
|
+
# value, and a mode, and we ignore the modes because there's no
|
789
|
+
# way for us to figure out absolute altitudes given, say,
|
790
|
+
# :relativeToGround
|
791
|
+
# ymin, ymax x1 y1
|
792
|
+
:altitude => Quadratic.interpolate(a.altitude, b.altitude, 0.5, bounce_alt),
|
793
|
+
:altitudeMode => a.altitudeMode,
|
794
|
+
:duration => duration * 1.0 / points,
|
795
|
+
}
|
796
|
+
opts[:no_flyto] = 1 if options.has_key?(:no_flyto)
|
797
|
+
opts[:show_placemarks] = 1 if options.has_key?(:show_placemarks)
|
728
798
|
|
729
|
-
|
730
|
-
|
799
|
+
if a.kind_of? Camera then
|
800
|
+
opts[:roll] = Line.interpolate(a.roll, b.roll)
|
801
|
+
else
|
802
|
+
opts[:range] = Line.interpolate(a.range, b.range)
|
803
|
+
end
|
804
|
+
return make_function_path(points, opts)
|
805
|
+
end
|
806
|
+
|
807
|
+
# Returns the great circle distance between two points
|
808
|
+
def great_circle_distance(a, b)
|
809
|
+
# Stolen from http://rosettacode.org/wiki/Haversine_formula#Ruby
|
810
|
+
|
811
|
+
def deg2rad(a)
|
812
|
+
a * Math::PI / 180
|
813
|
+
end
|
814
|
+
|
815
|
+
radius = 6371 # rough radius of the Earth, in kilometers
|
816
|
+
lat1, long1 = [Math::PI * a.latitude / 180.0, Math::PI * a.longitude / 180.0]
|
817
|
+
lat2, long2 = [Math::PI * b.latitude / 180.0, Math::PI * b.longitude / 180.0]
|
818
|
+
d = 2 * radius *
|
819
|
+
Math.asin( Math.sqrt(
|
820
|
+
Math.sin((lat2-lat1)/2)**2 +
|
821
|
+
Math.cos(lat1) * Math.cos(lat2) *
|
822
|
+
Math.sin((long2 - long1)/2)**2
|
823
|
+
))
|
824
|
+
|
825
|
+
return d
|
826
|
+
end
|
827
|
+
|
828
|
+
# Accepts an XML::Document object containing some placemarks. Creates a
|
829
|
+
# spline between those placemarks and flies along it.
|
830
|
+
#
|
831
|
+
# d: The XML::Document object
|
832
|
+
# num_points: The number of points to include on the final flight path
|
833
|
+
# ctrl_point_dur: Default duration for each control point. Overridden by
|
834
|
+
# the return value of ctrl_point_cb
|
835
|
+
# ctrl_point_cb: Callback function accepting a control point view and a
|
836
|
+
# sequence number, which returns the duration for this control point
|
837
|
+
#
|
838
|
+
# Yields each fly_to object and a sequence number, in case the user wants
|
839
|
+
# to manipulate them further.
|
840
|
+
def fly_placemarks(d, num_points = 30, ctrl_point_dur = 1, ctrl_point_cb = nil)
|
841
|
+
i = 0
|
842
|
+
sp = Kamelopard::Functions::ViewSplineFunction.new
|
843
|
+
|
844
|
+
each_placemark(d) do |p, v|
|
845
|
+
dur = ctrl_point_dur
|
846
|
+
if ! ctrl_point_cb.nil?
|
847
|
+
dur = ctrl_point_cb.call(p, i)
|
731
848
|
end
|
849
|
+
raise "The control point duration cannot be nil" if dur.nil?
|
850
|
+
sp.add_control_point(p, dur)
|
851
|
+
i += 1
|
852
|
+
end
|
732
853
|
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
d = 2 * radius *
|
737
|
-
Math.asin( Math.sqrt(
|
738
|
-
Math.sin((lat2-lat1)/2)**2 +
|
739
|
-
Math.cos(lat1) * Math.cos(lat2) *
|
740
|
-
Math.sin((long2 - long1)/2)**2
|
741
|
-
))
|
742
|
-
|
743
|
-
return d
|
854
|
+
(1..num_points).each do |i|
|
855
|
+
f = fly_to sp.run_function(i.to_f/30.0), :duration => 0.8, :mode => :smooth
|
856
|
+
yield f, i if block_given?
|
744
857
|
end
|
858
|
+
end
|