kamelopard 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,420 +1,591 @@
1
1
  # vim:ts=4:sw=4:et:smartindent:nowrap
2
- def fly_to(p, d = 0, r = 100, m = nil)
3
- m = Kamelopard::Document.instance.flyto_mode if m.nil?
4
- Kamelopard::FlyTo.new p, :range => r, :duration => d, :mode => m
5
- end
6
2
 
7
- def get_document()
8
- Kamelopard::Document.instance
9
- end
10
-
11
- def set_flyto_mode_to(a)
12
- Kamelopard::Document.instance.flyto_mode = a
13
- end
14
-
15
- def toggle_balloon_for(p, v, options = {})
16
- au = Kamelopard::AnimatedUpdate.new [], options
17
- if ! p.kind_of? Kamelopard::Placemark and ! p.kind_of? Kamelopard::ScreenOverlay then
18
- raise "Can't show balloons for things that aren't Placemarks or ScreenOverlays"
19
- end
20
- a = XML::Node.new 'Change'
21
- # XXX This can probably be more robust, based on just the class's name
22
- if p.kind_of? Kamelopard::Placemark then
23
- b = XML::Node.new 'Placemark'
24
- else
25
- b = XML::Node.new 'ScreenOverlay'
26
- end
27
- b.attributes['targetId'] = p.kml_id
28
- c = XML::Node.new 'gx:balloonVisibility'
29
- c << XML::Node.new_text(v.to_s)
30
- b << c
31
- a << b
32
- au << a
33
- end
34
-
35
- def hide_balloon_for(p, options = {})
36
- toggle_balloon_for(p, 0, options)
37
- end
38
-
39
- def show_balloon_for(p, options = {})
40
- toggle_balloon_for(p, 1, options)
41
- end
42
-
43
- def fade_balloon_for(p, v, options = {})
44
- au = Kamelopard::AnimatedUpdate.new [], options
45
- if ! p.is_a? Kamelopard::Placemark then
46
- raise "Can't show balloons for things that aren't placemarks"
47
- end
48
- a = XML::Node.new 'Change'
49
- b = XML::Node.new 'Placemark'
50
- b.attributes['targetId'] = p.kml_id
51
- c = XML::Node.new 'color'
52
- c << XML::Node.new_text(v.to_s)
53
- b << c
54
- a << b
55
- au << a
56
- end
57
-
58
- def fade_out_balloon_for(p, options = {})
59
- fade_balloon_for(p, '00ffffff', options)
60
- end
61
-
62
- def fade_in_balloon_for(p, options = {})
63
- fade_balloon_for(p, 'ffffffff', options)
64
- end
65
-
66
- def point(lo, la, alt=0, mode=nil, extrude = false)
67
- m = ( mode.nil? ? :clampToGround : mode )
68
- Kamelopard::Point.new(lo, la, alt, :altitudeMode => m, :extrude => extrude)
69
- end
70
-
71
- def placemark(name = nil, options = {})
72
- Kamelopard::Placemark.new name, options
73
- end
74
-
75
- # Returns the KML that makes up the current Kamelopard::Document, as a string.
76
- def get_kml
77
- Kamelopard::Document.instance.get_kml_document
78
- end
79
-
80
- def get_kml_string
81
- get_kml.to_s
82
- end
83
-
84
- def pause(p)
85
- Kamelopard::Wait.new p
86
- end
87
-
88
- def get_tour()
89
- Kamelopard::Document.instance.tour
90
- end
91
-
92
- def name_tour(a)
93
- Kamelopard::Document.instance.tour.name = a
94
- end
95
-
96
- def get_folder()
97
- Kamelopard::Document.instance.folders.last
98
- end
99
-
100
- def folder(name)
101
- Kamelopard::Folder.new(name)
102
- end
103
-
104
- def name_folder(a)
105
- Kamelopard::Document.instance.folder.name = a
106
- return Kamelopard::Document.instance.folder
107
- end
108
-
109
- def name_document(a)
110
- Kamelopard::Document.instance.name = a
111
- return Kamelopard::Document.instance
112
- end
113
-
114
- def zoom_out(dist = 1000, dur = 0, mode = nil)
115
- l = Kamelopard::Document.instance.tour.last_abs_view
116
- raise "No current position to zoom out from\n" if l.nil?
117
- l.range += dist
118
- Kamelopard::FlyTo.new(l, nil, dur, mode)
119
- end
120
-
121
- # Creates a list of FlyTo elements to orbit and look at a given point (center),
122
- # at a given range (in meters), starting and ending at given angles (in
123
- # degrees) from the center, where 0 and 360 (and -360, and 720, and -980, etc.)
124
- # are north. To orbit clockwise, make startHeading less than endHeading.
125
- # Otherwise, it will orbit counter-clockwise. To orbit multiple times, add or
126
- # subtract 360 from the endHeading. The tilt argument matches the KML LookAt
127
- # tilt argument
128
- def orbit(center, range = 100, tilt = 0, startHeading = 0, endHeading = 360)
129
- fly_to Kamelopard::LookAt.new(center, startHeading, tilt, range), 2, nil
130
-
131
- # We want at least 5 points (arbitrarily chosen value), plus at least 5 for
132
- # each full revolution
133
-
134
- # When I tried this all in one step, ruby told me 360 / 10 = 1805. I'm sure
135
- # there's some reason why this is a feature and not a bug, but I'd rather
136
- # not look it up right now.
137
- num = (endHeading - startHeading).abs
138
- den = ((endHeading - startHeading) / 360.0).to_i.abs * 5 + 5
139
- step = num / den
140
- step = 1 if step < 1
141
- step = step * -1 if startHeading > endHeading
142
-
143
- lastval = startHeading
144
- startHeading.step(endHeading, step) do |theta|
145
- lastval = theta
146
- fly_to Kamelopard::LookAt.new(center, theta, tilt, range), 2, nil, 'smooth'
147
- end
148
- if lastval != endHeading then
149
- fly_to Kamelopard::LookAt.new(center, endHeading, tilt, range), 2, nil, 'smooth'
150
- end
151
- end
152
-
153
- def sound_cue(href, ds = nil)
154
- Kamelopard::SoundCue.new href, ds
155
- end
156
-
157
- # 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
158
- # def orbit(center, range = 100, startHeading = 0, endHeading = 360)
159
- # p = ThreeDPointList.new()
160
- #
161
- # # Figure out how far we're going, and d
162
- # dist = endHeading - startHeading
163
- #
164
- # # We want at least 5 points (arbitrarily chosen value), plus at least 5 for each full revolution
165
- # step = (endHeading - startHeading) / ((endHeading - startHeading) / 360.0).to_i * 5 + 5
166
- # startHeading.step(endHeading, step) do |theta|
167
- # p << KMLPoint.new(
168
- # center.longitude + Math.cos(theta),
169
- # center.latitude + Math.sin(theta),
170
- # center.altitude, center.altitudeMode)
171
- # end
172
- # p << KMLPoint.new(
173
- # center.longitude + Math.cos(endHeading),
174
- # center.latitude + Math.sin(endHeading),
175
- # center.altitude, center.altitudeMode)
176
- #
177
- # p.interpolate.each do |a|
178
- # fly_to
179
- # end
180
- # end
181
-
182
- def set_prefix_to(a)
183
- Kamelopard.id_prefix = a
184
- end
185
-
186
- def write_kml_to(file = 'doc.kml')
187
- File.open(file, 'w') do |f| f.write get_kml.to_s end
188
- #File.open(file, 'w') do |f| f.write get_kml.to_s.gsub(/balloonVis/, 'gx:balloonVis') end
189
- end
190
-
191
- def fade_overlay(ov, show, options = {})
192
- color = '00ffffff'
193
- color = 'ffffffff' if show
194
- if ov.is_a? String then
195
- id = ov
196
- else
197
- id = ov.kml_id
198
- end
199
- k = Kamelopard::AnimatedUpdate.new "<Change><ScreenOverlay targetId=\"#{id}\"><color>#{color}</color></ScreenOverlay></Change>", options
200
- k
201
- end
202
-
203
- module TelemetryProcessor
204
- Pi = 3.1415926535
205
-
206
- def TelemetryProcessor.get_heading(p)
207
- x1, y1, x2, y2 = [ p[1][0], p[1][1], p[2][0], p[2][1] ]
208
-
209
- h = Math.atan((x2-x1) / (y2-y1)) * 180 / Pi
210
- h = h + 180.0 if y2 < y1
211
- h
212
- end
213
-
214
- def TelemetryProcessor.get_dist2(x1, y1, x2, y2)
215
- Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2).abs
216
- end
217
-
218
- def TelemetryProcessor.get_dist3(x1, y1, z1, x2, y2, z2)
219
- Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2 ).abs
220
- end
221
-
222
- def TelemetryProcessor.get_tilt(p)
223
- x1, y1, z1, x2, y2, z2 = [ p[1][0], p[1][1], p[1][2], p[2][0], p[2][1], p[2][2] ]
224
- smoothing_factor = 10.0
225
- dist = get_dist3(x1, y1, z1, x2, y2, z2)
226
- dist = dist + 1
227
- # + 1 to avoid setting dist to 0, and having div-by-0 errors later
228
- t = Math.atan((z2 - z1) / dist) * 180 / Pi / @@options[:exaggerate]
229
- # the / 2.0 is just because it looked nicer that way
230
- 90.0 + t
231
- end
232
-
233
- # roll = get_roll(last_last_lon, last_last_lat, last_lon, last_lat, lon, lat)
234
- def TelemetryProcessor.get_roll(p)
235
- x1, y1, x2, y2, x3, y3 = [ p[0][0], p[0][1], p[1][0], p[1][1], p[2][0], p[2][1] ]
236
- return 0 if x1.nil? or x2.nil?
237
-
238
- # Measure roll based on angle between P1 -> P2 and P2 -> P3. To be really
239
- # exact I ought to take into account altitude as well, but ... I don't want
240
- # to
241
-
242
- # Set x2, y2 as the origin
243
- xn1 = x1 - x2
244
- xn3 = x3 - x2
245
- yn1 = y1 - y2
246
- yn3 = y3 - y2
247
-
248
- # Use dot product to get the angle between the two segments
249
- angle = Math.acos( ((xn1 * xn3) + (yn1 * yn3)) / (get_dist2(0, 0, xn1, yn1).abs * get_dist2(0, 0, xn3, yn3).abs) ) * 180 / Pi
250
-
251
- # angle = angle > 90 ? 90 : angle
252
- @@options[:exaggerate] * (angle - 180)
253
- end
254
-
255
- def TelemetryProcessor.fix_coord(a)
256
- a = a - 360 if a > 180
257
- a = a + 360 if a < -180
258
- a
259
- end
260
-
261
- def TelemetryProcessor.add_flyto(p)
262
- # p is an array of three points, where p[0] is the earliest. Each point is itself an array of [longitude, latitude, altitude].
263
- p2 = TelemetryProcessor::normalize_points p
264
- p = p2
265
- heading = get_heading p
266
- tilt = get_tilt p
267
- # roll = get_roll(last_last_lon, last_last_lat, last_lon, last_lat, lon, lat)
268
- roll = get_roll p
269
- #p = Kamelopard::Point.new last_lon, last_lat, last_alt, { :altitudeMode => :absolute }
270
- point = Kamelopard::Point.new p[1][0], p[1][1], p[1][2], { :altitudeMode => :absolute }
271
- c = Kamelopard::Camera.new point, { :heading => heading, :tilt => tilt, :roll => roll, :altitudeMode => :absolute }
272
- f = Kamelopard::FlyTo.new c, { :duration => @@options[:pause], :mode => :smooth }
273
- f.comment = "#{p[1][0]} #{p[1][1]} #{p[1][2]} to #{p[2][0]} #{p[2][1]} #{p[2][2]}"
274
- end
275
-
276
- def TelemetryProcessor.options=(a)
277
- @@options = a
278
- end
279
-
280
- def TelemetryProcessor.normalize_points(p)
281
- # The whole point here is to prevent problems when you cross the poles or the dateline
282
- # This could have serious problems if points are really far apart, like
283
- # hundreds of degrees. This seems unlikely.
284
- lons = ((0..2).collect { |i| p[i][0] })
285
- lats = ((0..2).collect { |i| p[i][1] })
286
-
287
- lon_min, lon_max = lons.minmax
288
- lat_min, lat_max = lats.minmax
289
-
290
- if (lon_max - lon_min).abs > 200 then
291
- (0..2).each do |i|
292
- lons[i] += 360.0 if p[i][0] < 0
293
- end
294
- end
295
-
296
- if (lat_max - lat_min).abs > 200 then
297
- (0..2).each do |i|
298
- lats[i] += 360.0 if p[i][1] < 0
299
- end
300
- end
301
-
302
- return [
303
- [ lons[0], lats[0], p[0][2] ],
304
- [ lons[1], lats[1], p[1][2] ],
305
- [ lons[2], lats[2], p[2][2] ],
306
- ]
307
- end
308
- end
309
-
310
- def tour_from_points(points, options = {})
311
- options.merge!({
312
- :pause => 1,
313
- :exaggerate => 1
314
- }) { |key, old, new| old }
315
- TelemetryProcessor.options = options
316
- (0..(points.size-3)).each do |i|
317
- TelemetryProcessor::add_flyto points[i,3]
318
- end
319
- end
320
-
321
- def make_view_from(options = {})
322
- o = {}
323
- o.merge! options
324
- options.each do |k, v|
325
- o[k.to_sym] = v unless k.kind_of? Symbol
326
- end
327
-
328
- # Set defaults
329
- [
330
- [ :altitude, 0 ],
331
- [ :altitudeMode, :relativeToGround ],
332
- [ :latitude, 0 ],
333
- [ :longitude, 0 ],
334
- [ :tilt, 0 ],
335
- [ :heading, 0 ],
336
- ].each do |a|
337
- o[a[0]] = a[1] unless o.has_key? a[0]
338
- end
339
-
340
- p = point o[:longitude], o[:latitude], o[:altitude], o[:altitudeMode]
341
-
342
- if o.has_key? :roll then
343
- view = Kamelopard::Camera.new p
344
- else
345
- view = Kamelopard::LookAt.new p
346
- end
347
-
348
- [ :altitudeMode, :tilt, :heading, :timestamp, :timespan, :timestamp, :range, :roll, :viewerOptions ].each do |a|
349
- view.method("#{a.to_s}=").call(o[a]) if o.has_key? a
350
- end
351
-
352
- view
353
- end
354
-
355
- def screenoverlay(options = {})
356
- Kamelopard::ScreenOverlay.new options
357
- end
358
-
359
- def xy(x = 0.5, y = 0.5, xt = :fraction, yt = :fraction)
360
- Kamelopard::XY.new x, y, xt, yt
361
- end
362
-
363
- def iconstyle(href = nil, options = {})
364
- Kamelopard::IconStyle.new href, options
365
- end
366
-
367
- def labelstyle(scale = 1, options = {})
368
- Kamelopard::LabelStyle.new scale, options
369
- end
370
-
371
- def style(options = {})
372
- Kamelopard::Style.new options
373
- end
374
-
375
- def look_at(point = nil, options = {})
376
- Kamelopard::LookAt.new point, options
377
- end
378
-
379
- def camera(point = nil, options = {})
380
- Kamelopard::Camera.new point, options
381
- end
382
-
383
- def fly_to(view = nil, options = {})
384
- Kamelopard::FlyTo.new view, options
385
- end
386
-
387
- # k = an XML::Document containing KML
388
- # Pulls the Placemarks from the KML document and flys to each one in turn
389
- def each_placemark(d)
390
- i = 0
391
- d.find('//kml:Placemark').each do |p|
392
- all_values = {}
393
-
394
- # These fields are part of the abstractview
395
- view_fields = %w{ latitude longitude heading range tilt roll altitude altitudeMode gx:altitudeMode }
396
- # These are other field I'm interested in
397
- other_fields = %w{ description name }
398
- all_fields = view_fields.clone
399
- all_fields.concat(other_fields.clone)
400
- all_fields.each do |k|
401
- if k == 'gx:altitudeMode' then
402
- ix = k
403
- next unless p.find_first('kml:altitudeMode').nil?
404
- else
405
- ix = "kml:#{k}"
406
- end
407
- r = k == "gx:altitudeMode" ? :altitudeMode : k.to_sym
408
- tmp = p.find_first("descendant::#{ix}")
409
- next if tmp.nil?
410
- all_values[k == "gx:altitudeMode" ? :altitudeMode : k.to_sym ] = tmp.content
411
- end
412
- view_values = {}
413
- view_fields.each do |v| view_values[v] = all_values[v].clone if all_values.has_key? v end
414
- yield make_view_from(view_values), all_values
415
- end
416
- end
417
-
418
- def make_tour_index(erb = nil, options = {})
419
- get_document.make_tour_index(erb, options)
420
- end
3
+ # Returns the current Document object
4
+ def get_document()
5
+ Kamelopard::DocumentHolder.instance.current_document
6
+ end
7
+
8
+ # Changes the default FlyTo mode. Possible values are :smooth and :bounce
9
+ def set_flyto_mode_to(mode)
10
+ Kamelopard::DocumentHolder.instance.current_document.flyto_mode = mode
11
+ end
12
+
13
+ # Shows or hides the popup balloon for Placemark and ScreenOverlay objects.
14
+ # Arguments are the object; 0 or 1 to hide or show the balloon, respectively;
15
+ # and a hash of options to be added to the AnimatedUpdate object this
16
+ # function creates. Refer to the AnimatedUpdate documentation for details on
17
+ # possible options.
18
+ def toggle_balloon_for(obj, value, options = {})
19
+ au = Kamelopard::AnimatedUpdate.new [], options
20
+ if ! obj.kind_of? Kamelopard::Placemark and ! obj.kind_of? Kamelopard::ScreenOverlay then
21
+ raise "Can't show balloons for things that aren't Placemarks or ScreenOverlays"
22
+ end
23
+ a = XML::Node.new 'Change'
24
+ # XXX This can probably be more robust, based on just the class's name
25
+ if obj.kind_of? Kamelopard::Placemark then
26
+ b = XML::Node.new 'Placemark'
27
+ else
28
+ b = XML::Node.new 'ScreenOverlay'
29
+ end
30
+ b.attributes['targetId'] = obj.kml_id
31
+ c = XML::Node.new 'gx:balloonVisibility'
32
+ c << XML::Node.new_text(value.to_s)
33
+ b << c
34
+ a << b
35
+ au << a
36
+ end
37
+
38
+ # Hides the popup balloon for a Placemark or ScreenOverlay object. Require
39
+ # the object as the first argument, and takes a hash of options passed to the
40
+ # AnimatedUpdate object this functino creates. See also show_balloon_for and
41
+ # toggle_balloon_for
42
+ def hide_balloon_for(obj, options = {})
43
+ toggle_balloon_for(obj, 0, options)
44
+ end
45
+
46
+ # Displays the popup balloon for a Placemark or ScreenOverlay object. Require
47
+ # the object as the first argument, and takes a hash of options passed to the
48
+ # AnimatedUpdate object this functino creates. See also show_balloon_for and
49
+ # toggle_balloon_for
50
+ def show_balloon_for(obj, options = {})
51
+ toggle_balloon_for(obj, 1, options)
52
+ end
53
+
54
+ # Fades a placemark's popup balloon in or out. Takes as arguments the
55
+ # placemark object, 0 or 1 to hide or show the balloon, respectively, and a
56
+ # has of options to be passed to the AnimatedUpdate object created by this
57
+ # function. In order to have the balloon fade over some noticeable time, at
58
+ # minimum the :duration attribute in this hash should be set to some
59
+ # meaningful number of seconds.
60
+ def fade_balloon_for(obj, value, options = {})
61
+ au = Kamelopard::AnimatedUpdate.new [], options
62
+ if ! obj.is_a? Kamelopard::Placemark then
63
+ raise "Can't show balloons for things that aren't placemarks"
64
+ end
65
+ a = XML::Node.new 'Change'
66
+ b = XML::Node.new 'Placemark'
67
+ b.attributes['targetId'] = obj.kml_id
68
+ c = XML::Node.new 'color'
69
+ c << XML::Node.new_text(value.to_s)
70
+ b << c
71
+ a << b
72
+ au << a
73
+ end
74
+
75
+ # Refer to fade_balloon_for. This function only fades the balloon out.
76
+ def fade_out_balloon_for(obj, options = {})
77
+ fade_balloon_for(obj, '00ffffff', options)
78
+ end
79
+
80
+ # Refer to fade_balloon_for. This function only fades the balloon in.
81
+ def fade_in_balloon_for(p, options = {})
82
+ fade_balloon_for(p, 'ffffffff', options)
83
+ end
84
+
85
+ # Creates a Point object. Arguments are latitude, longitude, altitude,
86
+ # altitude mode, and extrude
87
+ def point(lo, la, alt=0, mode=nil, extrude = false)
88
+ m = ( mode.nil? ? :clampToGround : mode )
89
+ Kamelopard::Point.new(lo, la, alt, :altitudeMode => m, :extrude => extrude)
90
+ end
91
+
92
+ # Creates a Placemark with the given name. Other Placemark attributes are set
93
+ # in the options hash.
94
+ def placemark(name = nil, options = {})
95
+ Kamelopard::Placemark.new name, options
96
+ end
97
+
98
+ # Returns the KML that makes up the current Kamelopard::Document
99
+ def get_kml
100
+ Kamelopard::DocumentHolder.instance.current_document.get_kml_document
101
+ end
102
+
103
+ # Returns the KML that makes up the current Document, as a string
104
+ def get_kml_string
105
+ get_kml.to_s
106
+ end
107
+
108
+ # Inserts a KML gx:Wait element
109
+ def pause(p)
110
+ Kamelopard::Wait.new p
111
+ end
112
+
113
+ # Returns the current Tour object
114
+ def get_tour()
115
+ Kamelopard::DocumentHolder.instance.current_document.tour
116
+ end
117
+
118
+ # Sets a name for the current Tour
119
+ def name_tour(name)
120
+ Kamelopard::DocumentHolder.instance.current_document.tour.name = name
121
+ end
122
+
123
+ # Returns the current Folder object
124
+ def get_folder()
125
+ Kamelopard::DocumentHolder.instance.current_document.folders.last
126
+ end
127
+
128
+ # Creates a new Folder with the current name
129
+ def folder(name)
130
+ Kamelopard::Folder.new(name)
131
+ end
132
+
133
+ # Names (or renames) the current Folder, and returns it
134
+ def name_folder(name)
135
+ Kamelopard::DocumentHolder.instance.current_document.folder.name = name
136
+ return Kamelopard::DocumentHolder.instance.current_document.folder
137
+ end
138
+
139
+ # Names (or renames) the current Document object, and returns it
140
+ def name_document(name)
141
+ Kamelopard::DocumentHolder.instance.current_document.name = name
142
+ return Kamelopard::DocumentHolder.instance.current_document
143
+ end
144
+
145
+ def zoom_out(dist = 1000, dur = 0, mode = nil)
146
+ l = Kamelopard::DocumentHolder.instance.current_document.tour.last_abs_view
147
+ raise "No current position to zoom out from\n" if l.nil?
148
+ l.range += dist
149
+ Kamelopard::FlyTo.new(l, nil, dur, mode)
150
+ end
151
+
152
+ # Creates a list of FlyTo elements to orbit and look at a given point (center),
153
+ # at a given range (in meters), starting and ending at given angles (in
154
+ # degrees) from the center, where 0 and 360 (and -360, and 720, and -980, etc.)
155
+ # are north. To orbit clockwise, make startHeading less than endHeading.
156
+ # Otherwise, it will orbit counter-clockwise. To orbit multiple times, add or
157
+ # subtract 360 from the endHeading. The tilt argument matches the KML LookAt
158
+ # tilt argument
159
+ def orbit(center, range = 100, tilt = 0, startHeading = 0, endHeading = 360)
160
+ fly_to Kamelopard::LookAt.new(center, startHeading, tilt, range), 2, nil
161
+
162
+ # We want at least 5 points (arbitrarily chosen value), plus at least 5 for
163
+ # each full revolution
164
+
165
+ # When I tried this all in one step, ruby told me 360 / 10 = 1805. I'm sure
166
+ # there's some reason why this is a feature and not a bug, but I'd rather
167
+ # not look it up right now.
168
+ num = (endHeading - startHeading).abs
169
+ den = ((endHeading - startHeading) / 360.0).to_i.abs * 5 + 5
170
+ step = num / den
171
+ step = 1 if step < 1
172
+ step = step * -1 if startHeading > endHeading
173
+
174
+ lastval = startHeading
175
+ startHeading.step(endHeading, step) do |theta|
176
+ lastval = theta
177
+ fly_to Kamelopard::LookAt.new(center, theta, tilt, range), 2, nil, 'smooth'
178
+ end
179
+ if lastval != endHeading then
180
+ fly_to Kamelopard::LookAt.new(center, endHeading, tilt, range), 2, nil, 'smooth'
181
+ end
182
+ end
183
+
184
+ # Adds a SoundCue object.
185
+ def sound_cue(href, ds = nil)
186
+ Kamelopard::SoundCue.new href, ds
187
+ end
188
+
189
+ # 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
190
+ # def orbit(center, range = 100, startHeading = 0, endHeading = 360)
191
+ # p = ThreeDPointList.new()
192
+ #
193
+ # # Figure out how far we're going, and d
194
+ # dist = endHeading - startHeading
195
+ #
196
+ # # We want at least 5 points (arbitrarily chosen value), plus at least 5 for each full revolution
197
+ # step = (endHeading - startHeading) / ((endHeading - startHeading) / 360.0).to_i * 5 + 5
198
+ # startHeading.step(endHeading, step) do |theta|
199
+ # p << KMLPoint.new(
200
+ # center.longitude + Math.cos(theta),
201
+ # center.latitude + Math.sin(theta),
202
+ # center.altitude, center.altitudeMode)
203
+ # end
204
+ # p << KMLPoint.new(
205
+ # center.longitude + Math.cos(endHeading),
206
+ # center.latitude + Math.sin(endHeading),
207
+ # center.altitude, center.altitudeMode)
208
+ #
209
+ # p.interpolate.each do |a|
210
+ # fly_to
211
+ # end
212
+ # end
213
+
214
+ # Sets a prefix for all kml_id objects. Note that this does *not* change
215
+ # previously created objects' kml_ids... just new kml_ids going forward.
216
+ def set_prefix_to(a)
217
+ Kamelopard.id_prefix = a
218
+ end
219
+
220
+ # Writes KML output (and if applicable, viewsyncrelay configuration) to files.
221
+ # Include a file name for the actions_file argument to get viewsyncrelay
222
+ # configuration output as well. Note that this configuration includes only the
223
+ # actions section; users are responsible for creating appropriate linkages,
224
+ # inputs and outputs, and transformations, on their own, presumably in a
225
+ # separate file.
226
+ def write_kml_to(file = 'doc.kml', actions_file = 'actions.yml')
227
+ File.open(file, 'w') do |f| f.write get_kml.to_s end
228
+ if (get_document.vsr_actions.size > 0) then
229
+ File.open(actions_file, 'w') do |f| f.write get_document.get_actions end
230
+ end
231
+ #File.open(file, 'w') do |f| f.write get_kml.to_s.gsub(/balloonVis/, 'gx:balloonVis') end
232
+ end
233
+
234
+ # Fades a screen overlay in or out. The show argument is boolean; true to
235
+ # show the overlay, or false to hide it. The fade will happen smoothly (as
236
+ # opposed to immediately) if the options hash includes a :duration element
237
+ # set to some positive number of seconds.
238
+ def fade_overlay(ov, show, options = {})
239
+ color = '00ffffff'
240
+ color = 'ffffffff' if show
241
+ if ov.is_a? String then
242
+ id = ov
243
+ else
244
+ id = ov.kml_id
245
+ end
246
+
247
+ a = XML::Node.new 'Change'
248
+ b = XML::Node.new 'ScreenOverlay'
249
+ b.attributes['targetId'] = id
250
+ c = XML::Node.new 'color'
251
+ c << XML::Node.new_text(color)
252
+ b << c
253
+ a << b
254
+ k = Kamelopard::AnimatedUpdate.new [a], options
255
+ end
256
+
257
+ # Given telemetry data, such as from an aircraft, including latitude,
258
+ # longitude, and altitude, this will figure out realistic-looking tilt,
259
+ # heading, and roll values and create a series of FlyTo objects to follow the
260
+ # extrapolated flight path
261
+ module TelemetryProcessor
262
+ Pi = 3.1415926535
263
+
264
+ def TelemetryProcessor.get_heading(p)
265
+ x1, y1, x2, y2 = [ p[1][0], p[1][1], p[2][0], p[2][1] ]
266
+
267
+ h = Math.atan((x2-x1) / (y2-y1)) * 180 / Pi
268
+ h = h + 180.0 if y2 < y1
269
+ h
270
+ end
271
+
272
+ def TelemetryProcessor.get_dist2(x1, y1, x2, y2)
273
+ Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2).abs
274
+ end
275
+
276
+ def TelemetryProcessor.get_dist3(x1, y1, z1, x2, y2, z2)
277
+ Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2 ).abs
278
+ end
279
+
280
+ def TelemetryProcessor.get_tilt(p)
281
+ x1, y1, z1, x2, y2, z2 = [ p[1][0], p[1][1], p[1][2], p[2][0], p[2][1], p[2][2] ]
282
+ smoothing_factor = 10.0
283
+ dist = get_dist3(x1, y1, z1, x2, y2, z2)
284
+ dist = dist + 1
285
+ # + 1 to avoid setting dist to 0, and having div-by-0 errors later
286
+ t = Math.atan((z2 - z1) / dist) * 180 / Pi / @@options[:exaggerate]
287
+ # the / 2.0 is just because it looked nicer that way
288
+ 90.0 + t
289
+ end
290
+
291
+ def TelemetryProcessor.get_roll(p)
292
+ x1, y1, x2, y2, x3, y3 = [ p[0][0], p[0][1], p[1][0], p[1][1], p[2][0], p[2][1] ]
293
+ return 0 if x1.nil? or x2.nil?
294
+
295
+ # Measure roll based on angle between P1 -> P2 and P2 -> P3. To be really
296
+ # exact I ought to take into account altitude as well, but ... I don't want
297
+ # to
298
+
299
+ # Set x2, y2 as the origin
300
+ xn1 = x1 - x2
301
+ xn3 = x3 - x2
302
+ yn1 = y1 - y2
303
+ yn3 = y3 - y2
304
+
305
+ # Use dot product to get the angle between the two segments
306
+ angle = Math.acos( ((xn1 * xn3) + (yn1 * yn3)) / (get_dist2(0, 0, xn1, yn1).abs * get_dist2(0, 0, xn3, yn3).abs) ) * 180 / Pi
307
+
308
+ @@options[:exaggerate] * (angle - 180)
309
+ end
310
+
311
+ def TelemetryProcessor.fix_coord(a)
312
+ a = a - 360 if a > 180
313
+ a = a + 360 if a < -180
314
+ a
315
+ end
316
+
317
+ # This is the only function in the module that users are expected to
318
+ # call, and even then users should probably use the tour_from_points
319
+ # function. The p argument contains an ordered array of points, where
320
+ # each point is represented as an array consisting of longitude,
321
+ # latitude, and altitude, in that order. This will add a series of
322
+ # gx:FlyTo objects following the path determined by those points.
323
+ #--
324
+ # XXX Have some way to adjust FlyTo duration based on the distance
325
+ # between points, or based on user input.
326
+ #++
327
+ def TelemetryProcessor.add_flyto(p)
328
+ p2 = TelemetryProcessor::normalize_points p
329
+ p = p2
330
+ heading = get_heading p
331
+ tilt = get_tilt p
332
+ # roll = get_roll(last_last_lon, last_last_lat, last_lon, last_lat, lon, lat)
333
+ roll = get_roll p
334
+ #p = Kamelopard::Point.new last_lon, last_lat, last_alt, { :altitudeMode => :absolute }
335
+ point = Kamelopard::Point.new p[1][0], p[1][1], p[1][2], { :altitudeMode => :absolute }
336
+ c = Kamelopard::Camera.new point, { :heading => heading, :tilt => tilt, :roll => roll, :altitudeMode => :absolute }
337
+ f = Kamelopard::FlyTo.new c, { :duration => @@options[:pause], :mode => :smooth }
338
+ f.comment = "#{p[1][0]} #{p[1][1]} #{p[1][2]} to #{p[2][0]} #{p[2][1]} #{p[2][2]}"
339
+ end
340
+
341
+ def TelemetryProcessor.options=(a)
342
+ @@options = a
343
+ end
344
+
345
+ def TelemetryProcessor.normalize_points(p)
346
+ # The whole point here is to prevent problems when you cross the poles or the dateline
347
+ # This could have serious problems if points are really far apart, like
348
+ # hundreds of degrees. This seems unlikely.
349
+ lons = ((0..2).collect { |i| p[i][0] })
350
+ lats = ((0..2).collect { |i| p[i][1] })
351
+
352
+ lon_min, lon_max = lons.minmax
353
+ lat_min, lat_max = lats.minmax
354
+
355
+ if (lon_max - lon_min).abs > 200 then
356
+ (0..2).each do |i|
357
+ lons[i] += 360.0 if p[i][0] < 0
358
+ end
359
+ end
360
+
361
+ if (lat_max - lat_min).abs > 200 then
362
+ (0..2).each do |i|
363
+ lats[i] += 360.0 if p[i][1] < 0
364
+ end
365
+ end
366
+
367
+ return [
368
+ [ lons[0], lats[0], p[0][2] ],
369
+ [ lons[1], lats[1], p[1][2] ],
370
+ [ lons[2], lats[2], p[2][2] ],
371
+ ]
372
+ end
373
+ end
374
+
375
+ # Creates a tour from a series of points, using TelemetryProcessor::add_flyto.
376
+ #
377
+ # The first argument is an ordered array of points, where each point is
378
+ # represented as an array of longitude, latitude, and altitude (in meters),
379
+ # in that order. The only options currently recognized are :pause and
380
+ # :exaggerate. :pause controls the flight speed by specifying the duration of
381
+ # each FlyTo element. Its default is 1 second. There is currently no
382
+ # mechanism for having anything other than constant durations between points.
383
+ # :exaggerate is an numeric value that defaults to 1; when set to larger
384
+ # values, it will exaggerate tilt and roll values, because they're sort of
385
+ # boring at normal scale.
386
+ def tour_from_points(points, options = {})
387
+ options.merge!({
388
+ :pause => 1,
389
+ :exaggerate => 1
390
+ }) { |key, old, new| old }
391
+ TelemetryProcessor.options = options
392
+ (0..(points.size-3)).each do |i|
393
+ TelemetryProcessor::add_flyto points[i,3]
394
+ end
395
+ end
396
+
397
+ # Given a hash of values, this creates an AbstractView object. Possible
398
+ # values in the hash are :latitude, :longitude, :altitude, :altitudeMode,
399
+ # :tilt, :heading, :roll, and :range. If the hash specifies :roll, a Camera
400
+ # object will result; otherwise, a LookAt object will result. Specifying both
401
+ # :roll and :range will still result in a Camera object, and the :range
402
+ # option will be ignored. :roll and :range have no default; all other values
403
+ # default to 0 except :altitudeMode, which defaults to :relativeToGround
404
+ def make_view_from(options = {})
405
+ o = {}
406
+ o.merge! options
407
+ options.each do |k, v| o[k.to_sym] = v unless k.kind_of? Symbol
408
+ end
409
+
410
+ # Set defaults
411
+ [
412
+ [ :altitude, 0 ],
413
+ [ :altitudeMode, :relativeToGround ],
414
+ [ :latitude, 0 ],
415
+ [ :longitude, 0 ],
416
+ [ :tilt, 0 ],
417
+ [ :heading, 0 ],
418
+ ].each do |a|
419
+ o[a[0]] = a[1] unless o.has_key? a[0]
420
+ end
421
+
422
+ p = point o[:longitude], o[:latitude], o[:altitude], o[:altitudeMode]
423
+
424
+ if o.has_key? :roll then
425
+ view = Kamelopard::Camera.new p
426
+ else
427
+ view = Kamelopard::LookAt.new p
428
+ end
429
+
430
+ [ :altitudeMode, :tilt, :heading, :timestamp, :timespan, :timestamp, :range, :roll, :viewerOptions ].each do |a|
431
+ view.method("#{a.to_s}=").call(o[a]) if o.has_key? a
432
+ end
433
+
434
+ view
435
+ end
436
+
437
+ # Creates a ScreenOverlay object
438
+ def screenoverlay(options = {})
439
+ Kamelopard::ScreenOverlay.new options
440
+ end
441
+
442
+ # Creates an XY object, for use when building Overlay objects
443
+ def xy(x = 0.5, y = 0.5, xt = :fraction, yt = :fraction)
444
+ Kamelopard::XY.new x, y, xt, yt
445
+ end
446
+
447
+ # Creates an IconStyle object.
448
+ def iconstyle(href = nil, options = {})
449
+ Kamelopard::IconStyle.new href, options
450
+ end
451
+
452
+ # Creates an LabelStyle object.
453
+ def labelstyle(scale = 1, options = {})
454
+ Kamelopard::LabelStyle.new scale, options
455
+ end
456
+
457
+ # Creates an BalloonStyle object.
458
+ def balloonstyle(text, options = {})
459
+ Kamelopard::BalloonStyle.new text, options
460
+ end
461
+
462
+ # Creates an Style object.
463
+ def style(options = {})
464
+ Kamelopard::Style.new options
465
+ end
466
+
467
+ # Creates a LookAt object focused on the given point
468
+ def look_at(point = nil, options = {})
469
+ Kamelopard::LookAt.new point, options
470
+ end
471
+
472
+ # Creates a Camera object focused on the given point
473
+ def camera(point = nil, options = {})
474
+ Kamelopard::Camera.new point, options
475
+ end
476
+
477
+ # Creates a FlyTo object flying to the given AbstractView
478
+ def fly_to(view = nil, options = {})
479
+ Kamelopard::FlyTo.new view, options
480
+ end
481
+
482
+ # Pulls the Placemarks from the KML document d and yields each in turn to the caller
483
+ # k = an XML::Document containing KML
484
+ def each_placemark(d)
485
+ i = 0
486
+ d.find('//kml:Placemark').each do |p|
487
+ all_values = {}
488
+
489
+ # These fields are part of the abstractview
490
+ view_fields = %w{ latitude longitude heading range tilt roll altitude altitudeMode gx:altitudeMode }
491
+ # These are other field I'm interested in
492
+ other_fields = %w{ description name }
493
+ all_fields = view_fields.clone
494
+ all_fields.concat(other_fields.clone)
495
+ all_fields.each do |k|
496
+ if k == 'gx:altitudeMode' then
497
+ ix = k
498
+ next unless p.find_first('kml:altitudeMode').nil?
499
+ else
500
+ ix = "kml:#{k}"
501
+ end
502
+ r = k == "gx:altitudeMode" ? :altitudeMode : k.to_sym
503
+ tmp = p.find_first("descendant::#{ix}")
504
+ next if tmp.nil?
505
+ all_values[k == "gx:altitudeMode" ? :altitudeMode : k.to_sym ] = tmp.content
506
+ end
507
+ view_values = {}
508
+ 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
509
+ yield make_view_from(view_values), all_values
510
+ end
511
+ end
512
+
513
+ # Makes an HTML tour index, linked to a one-pixel screen overlay. The HTML
514
+ # contains links to start each tour.
515
+ def make_tour_index(erb = nil, options = {})
516
+ get_document.make_tour_index(erb, options)
517
+ end
518
+
519
+ # Superceded by toggle_balloon_for, but retained for backward compatibility
520
+ def show_hide_balloon(p, wait, options = {})
521
+ show_balloon_for p, options
522
+ pause wait
523
+ hide_balloon_for p, options
524
+ end
525
+
526
+ # Creates a CDATA XML::Node. This is useful for, among other things,
527
+ # ExtendedData values
528
+ def cdata(text)
529
+ XML::Node.new_cdata text.to_s
530
+ end
531
+
532
+ # Returns an array of two values, equal to l +/- p%, defining a "band" around the central value l
533
+ # NB! p is interpreted as a percentage, not a fraction. IOW the result is divided by 100.0.
534
+ def band(l, p)
535
+ f = l * p / 100.0
536
+ [ l - f, l + f ]
537
+ end
538
+
539
+
540
+ # Ensures v is within the range [min, max]. Modifies v to be within that range,
541
+ # assuming the number line is circular (as with latitude or longitude)
542
+ def circ_bounds(v, max, min)
543
+ w = max - min
544
+ if v > max then
545
+ while (v > max) do
546
+ v = v - w
547
+ end
548
+ elsif v < min then
549
+ while (v < min) do
550
+ v = v + w
551
+ end
552
+ end
553
+ v
554
+ end
555
+
556
+ # These functions ensure the given value is within appropriate bounds for a
557
+ # latitude or longitude. Modifies it as necessary if it's not.
558
+ def lat_check(l)
559
+ circ_bounds(l * 1.0, 90.0, -90.0)
560
+ end
561
+
562
+ # See lat_check()
563
+ def long_check(l)
564
+ circ_bounds(l * 1.0, 180.0, -180.0)
565
+ end
566
+
567
+ # Turns an array of two values (min, max) into a string suitable for use as a
568
+ # viewsyncrelay constraint
569
+ def to_constraint(arr)
570
+ "[#{arr[0]}, #{arr[1]}]"
571
+ end
572
+
573
+ # Adds a VSRAction object (a viewsyncrelay action) to the document, for
574
+ # viewsyncrelay configuration
575
+ def do_action(cmd, options = {})
576
+ end
577
+
578
+ # Returns the Document's VSRActions as a YAML string, suitable for writing to
579
+ # a viewsyncrelay configuration file
580
+ def get_actions
581
+ get_document.get_actions_yaml
582
+ end
583
+
584
+ # Writes actions to a viewsyncrelay config file
585
+ def write_actions_to(filename = 'actions.yml')
586
+ File.open(filename, 'w') do |f| f.write get_actions end
587
+ end
588
+
589
+ def get_doc_holder
590
+ return Kamelopard::DocumentHolder.instance
591
+ end