kamelopard 0.0.14 → 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,744 +1,858 @@
1
1
  #--
2
2
  # vim:ts=4:sw=4:et:smartindent:nowrap
3
3
  #++
4
- # Various helper functions
5
-
6
- # Returns the current Document object
7
- def get_document()
8
- Kamelopard::DocumentHolder.instance.current_document
9
- end
10
-
11
- # Changes the default FlyTo mode. Possible values are :smooth and :bounce
12
- def set_flyto_mode_to(mode)
13
- Kamelopard::DocumentHolder.instance.current_document.flyto_mode = mode
14
- end
15
-
16
- # Clears out the document_holder
17
- def clear_documents
18
- dh = get_doc_holder
19
- dh.delete_current_doc while dh.documents.size > 0
20
- end
21
-
22
-
23
- # Shows or hides the popup balloon for Placemark and ScreenOverlay objects.
24
- # Arguments are the object; 0 or 1 to hide or show the balloon, respectively;
25
- # and a hash of options to be added to the AnimatedUpdate object this
26
- # function creates. Refer to the AnimatedUpdate documentation for details on
27
- # possible options.
28
- def toggle_balloon_for(obj, value, options = {})
29
- au = Kamelopard::AnimatedUpdate.new [], options
30
- if ! obj.kind_of? Kamelopard::Placemark and ! obj.kind_of? Kamelopard::ScreenOverlay then
31
- raise "Can't show balloons for things that aren't Placemarks or ScreenOverlays"
32
- end
33
- a = XML::Node.new 'Change'
34
- # XXX This can probably be more robust, based on just the class's name
35
- if obj.kind_of? Kamelopard::Placemark then
36
- b = XML::Node.new 'Placemark'
37
- else
38
- b = XML::Node.new 'ScreenOverlay'
39
- end
40
- b.attributes['targetId'] = obj.kml_id
41
- c = XML::Node.new 'gx:balloonVisibility'
42
- c << XML::Node.new_text(value.to_s)
43
- b << c
44
- a << b
45
- au << a
46
- end
47
-
48
- # Hides the popup balloon for a Placemark or ScreenOverlay object. Require
49
- # the object as the first argument, and takes a hash of options passed to the
50
- # AnimatedUpdate object this functino creates. See also show_balloon_for and
51
- # toggle_balloon_for
52
- def hide_balloon_for(obj, options = {})
53
- toggle_balloon_for(obj, 0, options)
54
- end
55
-
56
- # Displays 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 show_balloon_for(obj, options = {})
61
- toggle_balloon_for(obj, 1, options)
62
- end
63
-
64
- # Fades a placemark's popup balloon in or out. Takes as arguments the
65
- # placemark object, 0 or 1 to hide or show the balloon, respectively, and a
66
- # has of options to be passed to the AnimatedUpdate object created by this
67
- # function. In order to have the balloon fade over some noticeable time, at
68
- # minimum the :duration attribute in this hash should be set to some
69
- # meaningful number of seconds.
70
- def fade_balloon_for(obj, value, options = {})
71
- au = Kamelopard::AnimatedUpdate.new [], options
72
- if ! obj.is_a? Kamelopard::Placemark then
73
- raise "Can't show balloons for things that aren't placemarks"
74
- end
75
- a = XML::Node.new 'Change'
76
- b = XML::Node.new 'Placemark'
77
- b.attributes['targetId'] = obj.kml_id
78
- c = XML::Node.new 'color'
79
- c << XML::Node.new_text(value.to_s)
80
- b << c
81
- a << b
82
- au << a
83
- end
84
-
85
- # Refer to fade_balloon_for. This function only fades the balloon out.
86
- def fade_out_balloon_for(obj, options = {})
87
- fade_balloon_for(obj, '00ffffff', options)
88
- end
89
-
90
- # Refer to fade_balloon_for. This function only fades the balloon in.
91
- def fade_in_balloon_for(p, options = {})
92
- fade_balloon_for(p, 'ffffffff', options)
93
- end
94
-
95
- # Creates a Point object. Arguments are latitude, longitude, altitude,
96
- # altitude mode, and extrude
97
- def point(lo, la, alt=0, mode=nil, extrude = false)
98
- m = ( mode.nil? ? :clampToGround : mode )
99
- Kamelopard::Point.new(lo, la, alt, :altitudeMode => m, :extrude => extrude)
100
- end
101
-
102
- # Creates a Placemark with the given name. Other Placemark attributes are set
103
- # in the options hash.
104
- def placemark(name = nil, options = {})
105
- Kamelopard::Placemark.new name, options
106
- end
107
-
108
- # Returns the KML that makes up the current Kamelopard::Document
109
- def get_kml
110
- Kamelopard::DocumentHolder.instance.current_document.get_kml_document
111
- end
112
-
113
- # Returns the KML that makes up the current Document, as a string
114
- def get_kml_string
115
- get_kml.to_s
116
- end
117
-
118
- # Inserts a KML gx:Wait element
119
- def pause(p, options = {})
120
- Kamelopard::Wait.new p, options
121
- end
122
-
123
- # Returns the current Tour object
124
- def get_tour()
125
- Kamelopard::DocumentHolder.instance.current_document.tour
126
- end
127
-
128
- # Sets a name for the current Tour
129
- def name_tour(name)
130
- Kamelopard::DocumentHolder.instance.current_document.tour.name = name
131
- end
132
-
133
- # Returns the current Folder object
134
- def get_folder()
135
- f = Kamelopard::DocumentHolder.instance.current_document.folders.last
136
- Kamelopard::Folder.new() if f.nil?
137
- Kamelopard::DocumentHolder.instance.current_document.folders.last
138
- end
139
-
140
- # Creates a new Folder with the current name
141
- def folder(name)
142
- Kamelopard::Folder.new(name)
143
- end
144
-
145
- # Names (or renames) the current Folder, and returns it
146
- def name_folder(name)
147
- Kamelopard::DocumentHolder.instance.current_document.folder.name = name
148
- return Kamelopard::DocumentHolder.instance.current_document.folder
149
- end
150
-
151
- # Names (or renames) the current Document object, and returns it
152
- def name_document(name)
153
- Kamelopard::DocumentHolder.instance.current_document.name = name
154
- return Kamelopard::DocumentHolder.instance.current_document
155
- end
156
-
157
- def zoom_out(dist = 1000, dur = 0, mode = nil)
158
- l = Kamelopard::DocumentHolder.instance.current_document.tour.last_abs_view
159
- raise "No current position to zoom out from\n" if l.nil?
160
- l.range += dist
161
- Kamelopard::FlyTo.new(l, nil, dur, mode)
162
- end
163
-
164
- # Translates a heading into something between 0 and 360
165
- def convert_heading(heading)
166
- if heading > 360 then
167
- step = -360
168
- else
169
- step = 360
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
- while heading < 0 or heading > 360 do
172
- heading = heading + step
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
- heading
175
- end
176
-
177
- # Creates a list of FlyTo elements to orbit and look at a given point (center),
178
- # at a given range (in meters), starting and ending at given angles (in
179
- # degrees) from the center, where 0 and 360 (and -360, and 720, and -980, etc.)
180
- # are north. To orbit multiple times, add or subtract 360 from the
181
- # endHeading.
182
- # The tilt argument matches the KML LookAt tilt argument
183
- # already_there, if true, means we've already flown to the initial point
184
- # The options hash can contain:
185
- # :duration The total duration of the orbit. Defaults to 0, which means it will take 2 seconds per step
186
- # :step How much to change the heading for each flyto. Defaults to some strange value >= 5
187
- # :already_there
188
- # Default false. Indicates that we've already flown to the initial position
189
- def orbit(center, range = 100, tilt = 90, startHeading = 0, endHeading = 360, options = {})
190
- duration = options.has_key?(:duration) ? options[:duration] : 0
191
- step = options.has_key?(:step) ? options[:step] : nil
192
- already_there = options.has_key?(:already_there) ? options[:already_there] : false
193
-
194
- am = center.altitudeMode
195
-
196
- if not step.nil? then
197
- if (endHeading - startHeading > 0 and step < 0) or (endHeading - startHeading < 0 and step > 0) then
198
- raise "Given start = #{startHeading}, end = #{endHeading}, and step = #{step}, this will be an infinite loop"
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
- # We want at least 5 points (arbitrarily chosen value), plus at least 5 for
203
- # each full revolution
204
-
205
- # When I tried this all in one step, ruby told me 360 / 10 = 1805. I'm sure
206
- # there's some reason why this is a feature and not a bug, but I'd rather
207
- # not look it up right now.
208
- dur = 2
209
- if step.nil? then
210
- num = (endHeading - startHeading).abs
211
- num_steps = ((endHeading - startHeading) / 360.0).to_i.abs * 5 + 5
212
- step = num / num_steps
213
- step = 1 if step < 1
214
- step = step * -1 if startHeading > endHeading
215
- if already_there
216
- num_steps = num_steps - 1
217
- startHeading = startHeading + step
218
- end
219
- if duration != 0
220
- dur = duration.to_f / num_steps
221
- end
222
- else
223
- dur = duration * 1.0 / ((endHeading - startHeading) * 1.0 / step) if duration != 0
224
- startHeading = startHeading + step if already_there
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
- lastval = startHeading
228
- mode = :bounce
229
- mode = :smooth if already_there
230
- startHeading.step(endHeading, step) do |theta|
231
- lastval = theta
232
- heading = convert_heading theta
233
- fly_to Kamelopard::LookAt.new(center, :heading => heading, :tilt => tilt, :range => range, :altitudeMode => am), :duration => dur, :mode => mode
234
- mode = :smooth
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
- if lastval != endHeading then
237
- fly_to Kamelopard::LookAt.new(center, :heading => convert_heading(endHeading), :tilt => tilt, :range => range, :altitudeMode => am), :duration => dur, :mode => :smooth
708
+ elsif v < min then
709
+ while (v < min) do
710
+ v = v + w
238
711
  end
239
712
  end
240
-
241
- # Adds a SoundCue object.
242
- def sound_cue(href, ds = nil)
243
- Kamelopard::SoundCue.new href, ds
244
- end
245
-
246
- # 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
247
- # def orbit(center, range = 100, startHeading = 0, endHeading = 360)
248
- # p = ThreeDPointList.new()
249
- #
250
- # # Figure out how far we're going, and d
251
- # dist = endHeading - startHeading
252
- #
253
- # # We want at least 5 points (arbitrarily chosen value), plus at least 5 for each full revolution
254
- # step = (endHeading - startHeading) / ((endHeading - startHeading) / 360.0).to_i * 5 + 5
255
- # startHeading.step(endHeading, step) do |theta|
256
- # p << KMLPoint.new(
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
- end
646
-
647
- # Adds a VSRAction object (a viewsyncrelay action) to the document, for
648
- # viewsyncrelay configuration
649
- def do_action(cmd, options = {})
650
- # XXX Finish this
651
- end
652
-
653
- # Returns the Document's VSRActions as a YAML string, suitable for writing to
654
- # a viewsyncrelay configuration file
655
- def get_actions
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
- end
743
+ end
658
744
 
659
- # Writes actions to a viewsyncrelay config file
660
- def write_actions_to(filename = 'actions.yml')
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
- end
748
+ end
663
749
 
664
- def get_doc_holder
665
- return Kamelopard::DocumentHolder.instance
666
- end
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
- # Generates a series of points in a path that will simulate Earth's FlyTo in
669
- # bounce mode, from one view to another. Note that the view objects must be
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
- opts = {
700
- :latitude => Line.interpolate(a.latitude, b.latitude),
701
- :longitude => Line.interpolate(a.longitude, b.longitude),
702
- :multidim => [[ latlonfunc, [ :latitude, :longitude ]]],
703
- :heading => Line.interpolate(a.heading, b.heading),
704
- :tilt => Line.interpolate(a.tilt, b.tilt),
705
- # XXX This doesn't really work. An actual altitude requires a
706
- # value, and a mode, and we ignore the modes because there's no
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
- # Returns the great circle distance between two points
726
- def great_circle_distance(a, b)
727
- # Stolen from http://rosettacode.org/wiki/Haversine_formula#Ruby
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
- def deg2rad(a)
730
- a * Math::PI / 180
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
- radius = 6371 # rough radius of the Earth, in kilometers
734
- lat1, long1 = [Math::PI * a.latitude / 180.0, Math::PI * a.longitude / 180.0]
735
- lat2, long2 = [Math::PI * b.latitude / 180.0, Math::PI * b.longitude / 180.0]
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