kamelopard 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1706 @@
1
+ # vim:ts=4:sw=4:et:smartindent:nowrap
2
+
3
+ # Classes to manage various KML objects. See
4
+ # http://code.google.com/apis/kml/documentation/kmlreference.html for a
5
+ # description of KML
6
+
7
+ require 'singleton'
8
+ require 'kamelopard/pointlist'
9
+
10
+ @@sequence = 0
11
+
12
+ def get_next_id # :nodoc
13
+ @@sequence += 1
14
+ @@sequence
15
+ end
16
+
17
+ #--
18
+ # Print out a set of kml fields. Expects an array argument. Each entry in the
19
+ # array is itself an array, containing two strings and a boolean. If the first
20
+ # string is nil, the function won't print anything for that element. If it's
21
+ # not null, it consults the boolean. True values tell the function to treat the
22
+ # second string as a KML element name, and print it along with XML decorators
23
+ # and the field value. False values mean just print the second string, with no
24
+ # decorators and no other values
25
+ #++
26
+ def kml_array(m, indent = 0) # :nodoc
27
+ k = ''
28
+ m.map do |a|
29
+ r = ''
30
+ if ! a[0].nil? then
31
+ if a[2] then
32
+ r << "#{ ' ' * indent}<" << a[1] << '>' << a[0].to_s << '</' << a[1] << ">\n"
33
+ else
34
+ r << a[1] << "\n"
35
+ end
36
+ end
37
+ k << r
38
+ end
39
+ k
40
+ end
41
+
42
+ #--
43
+ # Accepts XdX'X.X", XDXmX.XXs, XdXmX.XXs, or X.XXXX with either +/- or N/E/S/W
44
+ #++
45
+ def convert_coord(a) # :nodoc
46
+ a = a.to_s.upcase.strip
47
+
48
+ mult = 1
49
+ if a =~ /^-/ then
50
+ mult *= -1
51
+ end
52
+ a = a.sub /^\+|-/, ''
53
+ a = a.strip
54
+
55
+ if a =~ /[SW]$/ then
56
+ mult *= -1
57
+ end
58
+ a = a.sub /[NESW]$/, ''
59
+ a = a.strip
60
+
61
+ if a =~ /^\d+(\.\d+)?$/ then
62
+ # coord needs no transformation
63
+ 1
64
+ elsif a =~ /^\d+D\d+M\d+(\.\d+)?S$/ then
65
+ # coord is in dms
66
+ p = a.split /[D"']/
67
+ a = p[0].to_f + (p[2].to_f / 60.0 + p[1].to_f) / 60.0
68
+ elsif a =~ /^\d+D\d+'\d+(\.\d+)?"$/ then
69
+ # coord is in d'"
70
+ p = a.split /[D"']/
71
+ a = p[0].to_f + (p[2].to_f / 60.0 + p[1].to_f) / 60.0
72
+ else
73
+ raise "Couldn't determine coordinate format for #{a}"
74
+ end
75
+
76
+ # check that it's within range
77
+ a = a.to_f * mult
78
+ raise "Coordinate #{a} out of range" if a > 180 or a < -180
79
+ return a
80
+ end
81
+
82
+ # Base class for all Kamelopard objects. Manages object ID and a single
83
+ # comment string associated with the object
84
+ class KMLObject
85
+ attr_accessor :id, :comment
86
+
87
+ def initialize(comment = nil)
88
+ @id = "#{self.class.name}_#{ get_next_id }"
89
+ @comment = comment.gsub(/</, '&lt;') unless comment.nil?
90
+ end
91
+
92
+ # Returns KML string for this object. Objects should override this method
93
+ def to_kml(indent = 0)
94
+ if @comment.nil? or @comment == '' then
95
+ ''
96
+ else
97
+ "#{ ' ' * indent }<!-- #{ @comment } -->\n"
98
+ end
99
+ end
100
+ end
101
+
102
+ # Abstract base class for KMLPoint and several other classes
103
+ class Geometry < KMLObject
104
+ end
105
+
106
+ # Represents a Point in KML.
107
+ class KMLPoint < Geometry
108
+ attr_accessor :longitude, :latitude, :altitude, :altitudeMode, :extrude
109
+ def initialize(long, lat, alt=0, altmode=:clampToGround, extrude=false)
110
+ super()
111
+ @longitude = convert_coord(long)
112
+ @latitude = convert_coord(lat)
113
+ @altitude = alt
114
+ @altitudeMode = altmode
115
+ @extrude = extrude
116
+ end
117
+
118
+ def to_s
119
+ "KMLPoint (#{@longitude}, #{@latitude}, #{@altitude}, mode = #{@altitudeMode}, #{ @extrude ? 'extruded' : 'not extruded' })"
120
+ end
121
+
122
+ def to_kml(indent = 0, short = false)
123
+ # The short form includes only the coordinates tag
124
+ k = super(indent + 4) + "#{ ' ' * indent }<Point id=\"#{ @id }\">\n"
125
+ k << "#{ ' ' * indent } <extrude>#{ @extrude ? 1 : 0 }</extrude>\n" unless short
126
+ if not short then
127
+ if @altitudeMode == :clampToGround or @altitudeMode == :relativeToGround or @altitudeMode == :absolute then
128
+ k << "#{ ' ' * indent } <altitudeMode>#{ @altitudeMode }</altitudeMode>\n"
129
+ else
130
+ k << "#{ ' ' * indent } <gx:altitudeMode>#{ @altitudeMode }</gx:altitudeMode>\n"
131
+ end
132
+ end
133
+ k << "#{ ' ' * indent } <coordinates>#{ @longitude }, #{ @latitude }, #{ @altitude }</coordinates>\n"
134
+ k << "#{ ' ' * indent }</Point>\n"
135
+ k
136
+ end
137
+ end
138
+
139
+ # Helper class for KML objects which need to know about several points at once
140
+ class CoordinateList
141
+ attr_reader :coordinates
142
+
143
+ # Accepts an optional array of coordinates in any format add_element
144
+ # accepts
145
+ def initialize(coords = nil)
146
+ # Internally we store coordinates as an array of three-element
147
+ # arrays
148
+ @coordinates = []
149
+ if not coords.nil? then
150
+ add_element coords
151
+ else
152
+ @coordinates = []
153
+ end
154
+ end
155
+
156
+ def to_kml(indent = 0)
157
+ k = "#{ ' ' * indent }<coordinates>\n#{ ' ' * indent } "
158
+ if not @coordinates.nil? then
159
+ @coordinates.each do |a|
160
+ k << "#{ a[0] },#{ a[1] }"
161
+ k << ",#{ a[2] }" if a.size > 2
162
+ k << ' '
163
+ end
164
+ end
165
+ k << "\n#{ ' ' * indent}</coordinates>\n"
166
+ k
167
+ end
168
+
169
+ # Alias for add_element
170
+ def <<(a)
171
+ add_element a
172
+ end
173
+
174
+ # Adds one or more elements to this CoordinateList. The argument can be in any of several formats:
175
+ # * An array of arrays of numeric objects, in the form [ longitude,
176
+ # latitude, altitude (optional) ]
177
+ # * A KMLPoint, or some other object that response to latitude, longitude, and altitude methods
178
+ # * An array of the above
179
+ # * Another CoordinateList, to append to this on
180
+ # Note that this will not accept a one-dimensional array of numbers to add
181
+ # a single point. Instead, create a KMLPoint with those numbers, and pass
182
+ # it to add_element
183
+ def add_element(a)
184
+ if a.kind_of? Enumerable then
185
+ # We've got some sort of array or list. It could be a list of
186
+ # floats, to become one coordinate, or it could be several
187
+ # coordinates
188
+ t = a.to_a.first
189
+ if t.kind_of? Enumerable then
190
+ # At this point we assume we've got an array of float-like
191
+ # objects. The second-level arrays need to have two or three
192
+ # entries -- long, lat, and (optionally) alt
193
+ a.each do |i|
194
+ if i.size < 2 then
195
+ raise "There aren't enough objects here to make a 2- or 3-element coordinate"
196
+ elsif i.size >= 3 then
197
+ @coordinates << [ i[0].to_f, i[1].to_f, i[2].to_f ]
198
+ else
199
+ @coordinates << [ i[0].to_f, i[1].to_f ]
200
+ end
201
+ end
202
+ elsif t.respond_to? 'longitude' and
203
+ t.respond_to? 'latitude' and
204
+ t.respond_to? 'altitude' then
205
+ # This object can cough up a set of coordinates
206
+ a.each do |i|
207
+ @coordinates << [i.longitude, i.latitude, i.altitude]
208
+ end
209
+ else
210
+ # I dunno what it is
211
+ raise "Kamelopard can't understand this object as a coordinate"
212
+ end
213
+ elsif a.kind_of? CoordinateList then
214
+ # Append this coordinate list
215
+ @coordinates << a.coordinates
216
+ else
217
+ # This is one element. It better know how to make latitude, longitude, etc.
218
+ if a.respond_to? 'longitude' and
219
+ a.respond_to? 'latitude' and
220
+ a.respond_to? 'altitude' then
221
+ @coordinates << [a.longitude, a.latitude, a.altitude]
222
+ else
223
+ raise "Kamelopard can't understand this object as a coordinate"
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ # Corresponds to the KML LineString object
230
+ class LineString < Geometry
231
+ attr_accessor :altitudeOffset, :extrude, :tessellate, :altitudeMode, :drawOrder, :longitude, :latitude, :altitude
232
+ attr_reader :coordinates
233
+
234
+ def initialize(coords, altMode = :clampToGround)
235
+ super()
236
+ @altitudeMode = altMode
237
+ set_coords coords
238
+ end
239
+
240
+ # Sets @coordinates element
241
+ def set_coords(a)
242
+ if a.kind_of? CoordinateList then
243
+ @coordinates = a
244
+ else
245
+ @coordinates = CoordinateList.new(a)
246
+ end
247
+ end
248
+
249
+ # Appends an element to this LineString's CoordinateList. See CoordinateList#add_element
250
+ def <<(a)
251
+ @coordinates << a
252
+ end
253
+
254
+ def to_kml(indent = 0)
255
+
256
+ k = super(indent + 4) + "#{ ' ' * indent }<LineString id =\"#{ @id }\">\n"
257
+ k << kml_array([
258
+ [@altitudeOffset, 'gx:altitudeOffset', true],
259
+ [@extrude, 'extrude', true],
260
+ [@tessellate, 'tessellate', true],
261
+ [@drawOrder, 'gx:drawOrder', true]
262
+ ], indent + 4)
263
+ k << @coordinates.to_kml(indent + 4) unless @coordinates.nil?
264
+ if @altitudeMode == :clampToGround or @altitudeMode == :relativeToGround or @altitudeMode == :absolute then
265
+ k << "#{ ' ' * indent } <altitudeMode>#{ @altitudeMode }</altitudeMode>\n"
266
+ else
267
+ k << "#{ ' ' * indent } <gx:altitudeMode>#{ @altitudeMode }</gx:altitudeMode>\n"
268
+ end
269
+ k << "#{ ' ' * indent }</LineString>\n"
270
+ k
271
+ end
272
+ end
273
+
274
+ # Corresponds to KML's LinearRing object
275
+ class LinearRing < Geometry
276
+ attr_accessor :altitudeOffset, :extrude, :tessellate, :altitudeMode, :coordinates
277
+
278
+ def initialize(coordinates = nil, tessellate = 0, extrude = 0, altitudeMode = :clampToGround, altitudeOffset = nil)
279
+ super()
280
+ if coordinates.nil? then
281
+ @coordinates = nil
282
+ elsif coordinates.kind_of? CoordinateList then
283
+ @coordinates = coordinates
284
+ else
285
+ @coordinates = CoordinateList.new(coordinates)
286
+ end
287
+ @tessellate = tessellate
288
+ @extrude = extrude
289
+ @altitudeMode = altitudeMode
290
+ @altitudeOffset = altitudeOffset
291
+ end
292
+
293
+ # Sets @coordinates element
294
+ def set_coords(a)
295
+ if a.kind_of? CoordinateList then
296
+ @coordinates = a
297
+ else
298
+ @coordinates = CoordinateList.new(a)
299
+ end
300
+ end
301
+
302
+ # Appends an element to this LinearRing's CoordinateList. See CoordinateList#add_element
303
+ def <<(a)
304
+ @coordinates << a
305
+ end
306
+
307
+ def to_kml(indent = 0)
308
+ k = super(indent + 4) + "#{ ' ' * indent }<LinearRing id=\"#{ @id }\">\n"
309
+ k << "#{ ' ' * indent } <gx:altitudeOffset>#{ @altitudeOffset }</gx:altitudeOffset>\n" unless @altitudeOffset.nil?
310
+ k << "#{ ' ' * indent } <tessellate>#{ @tessellate }</tessellate>\n" unless @tessellate.nil?
311
+ k << "#{ ' ' * indent } <extrude>#{ @extrude }</extrude>\n" unless @extrude.nil?
312
+ if not @altitudeMode.nil? then
313
+ if @altitudeMode == :clampToGround or @altitudeMode == :relativeToGround or @altitudeMode == :absolute then
314
+ k << "#{ ' ' * indent } <altitudeMode>#{ @altitudeMode }</altitudeMode>\n"
315
+ else
316
+ k << "#{ ' ' * indent } <gx:altitudeMode>#{ @altitudeMode }</gx:altitudeMode>\n"
317
+ end
318
+ end
319
+ k << @coordinates.to_kml(indent + 4)
320
+ k << "#{ ' ' * indent }</LinearRing>\n"
321
+ k
322
+ end
323
+ end
324
+
325
+ # Abstract class corresponding to KML's AbstractView object
326
+ class AbstractView < KMLObject
327
+ attr_accessor :timestamp, :timespan, :options, :point, :heading, :tilt, :roll, :range, :altitudeMode
328
+ def initialize(className, point = nil, heading = 0, tilt = 0, roll = 0, range = 0, altitudeMode = :clampToGround)
329
+ raise "className argument must not be nil" if className.nil?
330
+ super()
331
+ @point = point
332
+ @options = {}
333
+ @className = className
334
+ if point.nil? then
335
+ @point = nil
336
+ elsif point.kind_of? Placemark then
337
+ @point = point.point
338
+ else
339
+ @point = point
340
+ end
341
+ @heading = heading
342
+ @tilt = tilt
343
+ @roll = roll
344
+ @range = range
345
+ @altitudeMode = altitudeMode
346
+ end
347
+
348
+ def point=(point)
349
+ @point.longitude = point.longitude
350
+ @point.latitude = point.latitude
351
+ @point.altitude = point.altitude
352
+ end
353
+
354
+ def longitude
355
+ @point.nil? ? nil : @point.longitude
356
+ end
357
+
358
+ def latitude
359
+ @point.nil? ? nil : @point.latitude
360
+ end
361
+
362
+ def altitude
363
+ @point.nil? ? nil : @point.altitude
364
+ end
365
+
366
+ def longitude=(a)
367
+ if @point.nil? then
368
+ @point = KMLPoint.new(a, 0)
369
+ else
370
+ @point.longitude = a
371
+ end
372
+ end
373
+
374
+ def latitude=(a)
375
+ if @point.nil? then
376
+ @point = KMLPoint.new(0, a)
377
+ else
378
+ @point.latitude = a
379
+ end
380
+ end
381
+
382
+ def altitude=(a)
383
+ if @point.nil? then
384
+ @point = KMLPoint.new(0, 0, a)
385
+ else
386
+ @point.altitude = a
387
+ end
388
+ end
389
+
390
+ def to_kml(indent = 0)
391
+ t = "#{ ' ' * indent }<#{ @className } id=\"#{ @id }\">\n"
392
+ t << super(indent)
393
+ t << kml_array([
394
+ [ @point.nil? ? nil : @point.longitude, 'longitude', true ],
395
+ [ @point.nil? ? nil : @point.latitude, 'latitude', true ],
396
+ [ @point.nil? ? nil : @point.altitude, 'altitude', true ],
397
+ [ @heading, 'heading', true ],
398
+ [ @tilt, 'tilt', true ],
399
+ [ @range, 'range', true ],
400
+ [ @roll, 'roll', true ]
401
+ ], indent + 4)
402
+ if @altitudeMode == :clampToGround or @altitudeMode == :relativeToGround or @altitudeMode == :absolute then
403
+ t << "#{ ' ' * indent } <altitudeMode>#{ @altitudeMode }</altitudeMode>\n"
404
+ else
405
+ t << "#{ ' ' * indent } <gx:altitudeMode>#{ @altitudeMode }</gx:altitudeMode>\n"
406
+ end
407
+ if @options.keys.length > 0 then
408
+ t << "#{ ' ' * indent } <gx:ViewerOptions>\n"
409
+ @options.each do |k, v|
410
+ t << "#{ ' ' * ( indent + 8 ) }<gx:option name=\"#{ k }\" enabled=\"#{ v ? 'true' : 'false' }\" />\n"
411
+ end
412
+ t << "#{ ' ' * indent } </gx:ViewerOptions>\n"
413
+ end
414
+ if not @timestamp.nil? then
415
+ t << @timestamp.to_kml(indent+4, 'gx')
416
+ elsif not @timespan.nil? then
417
+ t << @timespan.to_kml(indent+4, 'gx')
418
+ end
419
+ t << "#{ ' ' * indent }</#{ @className }>\n"
420
+ t
421
+ end
422
+
423
+ def [](a)
424
+ return @options[a]
425
+ end
426
+
427
+ def []=(a, b)
428
+ if not b.kind_of? FalseClass and not b.kind_of? TrueClass then
429
+ raise 'Option value must be boolean'
430
+ end
431
+ if a != :streetview and a != :historicalimagery and a != :sunlight then
432
+ raise 'Option index must be :streetview, :historicalimagery, or :sunlight'
433
+ end
434
+ @options[a] = b
435
+ end
436
+ end
437
+
438
+ # Corresponds to KML's Camera object
439
+ class Camera < AbstractView
440
+ def initialize(point = nil, heading = 0, tilt = 0, roll = 0, altitudeMode = :clampToGround)
441
+ super('Camera', point, heading, tilt, roll, nil, altitudeMode)
442
+ end
443
+
444
+ def range
445
+ raise "The range element is part of LookAt objects, not Camera objects"
446
+ end
447
+
448
+ def range=
449
+ # The range element doesn't exist in Camera objects
450
+ end
451
+ end
452
+
453
+ # Corresponds to KML's LookAt object
454
+ class LookAt < AbstractView
455
+ def initialize(point = nil, heading = 0, tilt = 0, range = 0, altitudeMode = :clampToGround)
456
+ super('LookAt', point, heading, tilt, nil, range, altitudeMode)
457
+ end
458
+
459
+ def roll
460
+ raise "The roll element is part of Camera objects, not LookAt objects"
461
+ end
462
+
463
+ def roll=
464
+ # The roll element doesn't exist in LookAt objects
465
+ end
466
+ end
467
+
468
+ # Abstract class corresponding to KML's TimePrimitive object
469
+ class TimePrimitive < KMLObject
470
+ end
471
+
472
+ # Corresponds to KML's TimeStamp object. The @when attribute must be in a format KML understands.
473
+ class TimeStamp < TimePrimitive
474
+ attr_accessor :when
475
+ def initialize(t_when)
476
+ super()
477
+ @when = t_when
478
+ end
479
+
480
+ def to_kml(indent = 0, ns = nil)
481
+ prefix = ''
482
+ prefix = ns + ':' unless ns.nil?
483
+
484
+ k = super(indent + 4)
485
+ k << <<-timestamp
486
+ #{ ' ' * indent }<#{ prefix }TimeStamp id="#{ @id }">
487
+ #{ ' ' * indent } <when>#{ @when }</when>
488
+ #{ ' ' * indent }</#{ prefix }TimeStamp>
489
+ timestamp
490
+ end
491
+ end
492
+
493
+ # Corresponds to KML's TimeSpan object. @begin and @end must be in a format KML
494
+ # understands.
495
+ class TimeSpan < TimePrimitive
496
+ attr_accessor :begin, :end
497
+ def initialize(t_begin, t_end)
498
+ super()
499
+ @begin = t_begin
500
+ @end = t_end
501
+ end
502
+
503
+ def to_kml(indent = 0, ns = nil)
504
+ prefix = ''
505
+ prefix = ns + ':' unless ns.nil?
506
+
507
+ k = super(indent + 4) + "#{ ' ' * indent }<#{ prefix }TimeSpan id=\"#{ @id }\">\n"
508
+ k << "#{ ' ' * indent } <begin>#{ @begin }</begin>\n" unless @begin.nil?
509
+ k << "#{ ' ' * indent } <end>#{ @end }</end>\n" unless @end.nil?
510
+ k << "#{ ' ' * indent }</#{ prefix }TimeSpan>\n"
511
+ k
512
+ end
513
+ end
514
+
515
+ # Support class for Feature object
516
+ class Snippet
517
+ attr_accessor :text, :maxLines
518
+ def initialize(text = nil, maxLines = 2)
519
+ @text = text
520
+ @maxLines = maxLines
521
+ end
522
+
523
+ def to_kml(indent = 0)
524
+ k = "#{ ' ' * indent }<Snippet maxLines=\"#{maxLines}\">"
525
+ k << text
526
+ k << "#{ ' ' * indent }</Snippet>\n"
527
+ end
528
+ end
529
+
530
+ # Abstract class corresponding to KML's Feature object.
531
+ class Feature < KMLObject
532
+ # Abstract class
533
+ attr_accessor :visibility, :open, :atom_author, :atom_link, :name,
534
+ :phoneNumber, :snippet, :description, :abstractView,
535
+ :timeprimitive, :styleUrl, :styleSelector, :region, :metadata,
536
+ :extendedData, :styles
537
+ attr_reader :addressDetails
538
+
539
+ def initialize (name = nil)
540
+ super()
541
+ @name = name
542
+ @visibility = true
543
+ @open = false
544
+ @styles = []
545
+ end
546
+
547
+ def timestamp
548
+ @timeprimitive
549
+ end
550
+
551
+ def timespan
552
+ @timeprimitive
553
+ end
554
+
555
+ def timestamp=(t)
556
+ @timeprimitive = t
557
+ end
558
+
559
+ def timespan=(t)
560
+ @timeprimitive = t
561
+ end
562
+
563
+ def addressDetails=(a)
564
+ if a.nil? or a == '' then
565
+ Document.instance.uses_xal = false
566
+ else
567
+ Document.instance.uses_xal = true
568
+ end
569
+ @addressDetails = a
570
+ end
571
+
572
+ # This function accepts either a StyleSelector object, or a string
573
+ # containing the desired StyleSelector's @id
574
+ def styleUrl=(a)
575
+ if a.is_a? String then
576
+ @styleUrl = a
577
+ elsif a.respond_to? 'id' then
578
+ @styleUrl = "##{ a.id }"
579
+ else
580
+ @styleUrl = a.to_s
581
+ end
582
+ end
583
+
584
+ def to_kml(indent = 0)
585
+ k = ''
586
+ if self.class == Feature then k << "#{ ' ' * indent }<Feature id=\"#{ @id }\">\n" end
587
+ k << super
588
+ k << kml_array([
589
+ [@name, 'name', true],
590
+ [(@visibility.nil? || @visibility) ? 1 : 0, 'visibility', true],
591
+ [(! @open.nil? && @open) ? 1 : 0, 'open', true],
592
+ [@atom_author, "<atom:author><atom:name>#{ @atom_author }</atom:name></atom:author>", false],
593
+ [@atom_link, 'atom:link', true],
594
+ [@address, 'address', true],
595
+ [@addressDetails, 'xal:AddressDetails', true],
596
+ [@phoneNumber, 'phoneNumber', true],
597
+ [@description, 'description', true],
598
+ [@styleUrl, 'styleUrl', true],
599
+ [@styleSelector, "<styleSelector>#{@styleSelector.nil? ? '' : @styleSelector.to_kml}</styleSelector>", false ],
600
+ [@metadata, 'Metadata', true ],
601
+ [@extendedData, 'ExtendedData', true ]
602
+ ], (indent))
603
+ k << styles_to_kml(indent)
604
+ k << @snippet.to_kml(indent) unless @snippet.nil?
605
+ k << @abstractView.to_kml(indent) unless @abstractView.nil?
606
+ k << @timeprimitive.to_kml(indent) unless @timeprimitive.nil?
607
+ k << @region.to_kml(indent) unless @region.nil?
608
+ k << yield if block_given?
609
+ if self.class == Feature then k << "#{ ' ' * indent }</Feature>\n" end
610
+ k
611
+ end
612
+
613
+ def styles_to_kml(indent = 0)
614
+ k = ''
615
+ @styles.each do |a|
616
+ k << a.to_kml(indent)
617
+ end
618
+ k
619
+ end
620
+ end
621
+
622
+ # Abstract class corresponding to KML's Container object.
623
+ class Container < Feature
624
+ def initialize
625
+ super
626
+ @features = []
627
+ end
628
+
629
+ # Adds a new object to this container.
630
+ def <<(a)
631
+ @features << a
632
+ end
633
+ end
634
+
635
+ # Corresponds to KML's Folder object.
636
+ class Folder < Container
637
+ attr_accessor :styles, :folders, :parent_folder
638
+
639
+ def initialize(name = nil)
640
+ super()
641
+ @name = name
642
+ @styles = []
643
+ @folders = []
644
+ Document.instance.folders << self
645
+ end
646
+
647
+ def to_kml(indent = 0)
648
+ h = "#{ ' ' * indent }<Folder id=\"#{@id}\">\n"
649
+ h << super(indent + 4)
650
+ @features.each do |a|
651
+ h << a.to_kml(indent + 4)
652
+ end
653
+ @folders.each do |a|
654
+ h << a.to_kml(indent + 4)
655
+ end
656
+ h << "#{ ' ' * indent }</Folder>\n";
657
+ h
658
+ end
659
+
660
+ # Folders can have parent folders; returns true if this folder has one
661
+ def has_parent?
662
+ not @parent_folder.nil?
663
+ end
664
+
665
+ # Folders can have parent folders; sets this folder's parent
666
+ def parent_folder=(a)
667
+ @parent_folder = a
668
+ a.folders << self
669
+ end
670
+ end
671
+
672
+ def get_stack_trace # :nodoc
673
+ k = ''
674
+ caller.each do |a| k << "#{a}\n" end
675
+ k
676
+ end
677
+
678
+ # Represents KML's Document class. This is a Singleton object; Kamelopard
679
+ # scripts can (for now) manage only one Document at a time.
680
+ class Document < Container
681
+ include Singleton
682
+ attr_accessor :flyto_mode, :folders, :tours, :uses_xal
683
+
684
+ def initialize
685
+ @tours = []
686
+ @folders = []
687
+ @styles = []
688
+ end
689
+
690
+ # Returns the current Tour object
691
+ def tour
692
+ @tours << Tour.new if @tours.length == 0
693
+ @tours.last
694
+ end
695
+
696
+ # Returns the current Folder object
697
+ def folder
698
+ if @folders.size == 0 then
699
+ Folder.new
700
+ end
701
+ @folders.last
702
+ end
703
+
704
+ def styles_to_kml(indent)
705
+ ''
706
+ end
707
+
708
+ def to_kml
709
+ xal = ''
710
+ if @uses_xal then
711
+ xal = ' xmlns:xal="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"'
712
+ end
713
+ h = <<-doc_header
714
+ <?xml version="1.0" encoding="UTF-8"?>
715
+ <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom"#{ xal }>
716
+ <Document>
717
+ doc_header
718
+
719
+ h << super(4)
720
+
721
+ # Print styles first
722
+ @styles.map do |a| h << a.to_kml(4) unless a.attached? end
723
+
724
+ # then folders
725
+ @folders.map do |a| h << a.to_kml(4) unless a.has_parent? end
726
+
727
+ # then tours
728
+ @tours.map do |a| h << a.to_kml(4) end
729
+ h << "</Document>\n</kml>\n"
730
+
731
+ h
732
+ end
733
+ end
734
+
735
+ # Corresponds to KML's ColorStyle object. Color is stored as an 8-character hex
736
+ # string, with two characters each of alpha, blue, green, and red values, in
737
+ # that order, matching the ordering the KML spec demands.
738
+ class ColorStyle < KMLObject
739
+ attr_accessor :color
740
+ attr_reader :colormode
741
+
742
+ def initialize(color, colormode = :normal)
743
+ super()
744
+ # Note: color element order is aabbggrr
745
+ @color = color
746
+ validate_colormode colormode
747
+ @colormode = colormode # Can be :normal or :random
748
+ end
749
+
750
+ def validate_colormode(a)
751
+ raise "colorMode must be either \"normal\" or \"random\"" unless a == :normal or a == :random
752
+ end
753
+
754
+ def colormode=(a)
755
+ validate_colormode a
756
+ @colormode = a
757
+ end
758
+
759
+ def alpha
760
+ @color[0,1]
761
+ end
762
+
763
+ def alpha=(a)
764
+ @color[0,1] = a
765
+ end
766
+
767
+ def blue
768
+ @color[2,1]
769
+ end
770
+
771
+ def blue=(a)
772
+ @color[2,1] = a
773
+ end
774
+
775
+ def green
776
+ @color[4,1]
777
+ end
778
+
779
+ def green=(a)
780
+ @color[4,1] = a
781
+ end
782
+
783
+ def red
784
+ @color[6,1]
785
+ end
786
+
787
+ def red=(a)
788
+ @color[6,1] = a
789
+ end
790
+
791
+ def to_kml(indent = 0)
792
+
793
+ super + <<-colorstyle
794
+ #{ ' ' * indent }<color>#{@color}</color>
795
+ #{ ' ' * indent }<colorMode>#{@colormode}</colorMode>
796
+ colorstyle
797
+ end
798
+ end
799
+
800
+ # Corresponds to KML's BalloonStyle object. Color is stored as an 8-character hex
801
+ # string, with two characters each of alpha, blue, green, and red values, in
802
+ # that order, matching the ordering the KML spec demands.
803
+ class BalloonStyle < ColorStyle
804
+ attr_accessor :bgcolor, :text, :textcolor, :displaymode
805
+
806
+ # Note: color element order is aabbggrr
807
+ def initialize(text = '', textcolor = 'ff000000', bgcolor = 'ffffffff', displaymode = :default)
808
+ super(nil, :normal)
809
+ @bgcolor = bgcolor
810
+ @text = text
811
+ @textcolor = textcolor
812
+ @displaymode = displaymode
813
+ end
814
+
815
+ def to_kml(indent = 0)
816
+ super + <<-balloonstyle
817
+ #{ ' ' * indent }<BalloonStyle id="#{@id}">
818
+ #{ ' ' * indent } <bgColor>#{@bgcolor}</bgColor>
819
+ #{ ' ' * indent } <text>#{@text}</text>
820
+ #{ ' ' * indent } <textColor>#{@textcolor}</textColor>
821
+ #{ ' ' * indent } <displayMode>#{@displaymode}</displayMode>
822
+ #{ ' ' * indent }</BalloonStyle>
823
+ balloonstyle
824
+ end
825
+ end
826
+
827
+ # Internal class used where KML requires X and Y values and units
828
+ class KMLxy
829
+ attr_accessor :x, :y, :xunits, :yunits
830
+ def initialize(x = 0.5, y = 0.5, xunits = :fraction, yunits = :fraction)
831
+ @x = x
832
+ @y = y
833
+ @xunits = xunits
834
+ @yunits = yunits
835
+ end
836
+
837
+ def to_kml(name, indent = 0)
838
+
839
+ <<-kmlxy
840
+ #{ ' ' * indent}<#{ name } x="#{ @x }" y="#{ @y }" xunits="#{ @xunits }" yunits="#{ @yunits }" />
841
+ kmlxy
842
+ end
843
+ end
844
+
845
+ # Corresponds to the KML Icon object
846
+ class Icon
847
+ attr_accessor :href, :x, :y, :w, :h, :refreshMode, :refreshInterval, :viewRefreshMode, :viewRefreshTime, :viewBoundScale, :viewFormat, :httpQuery
848
+
849
+ def initialize(href = nil)
850
+ @href = href
851
+ end
852
+
853
+ def to_kml(indent = 0)
854
+ k = "#{ ' ' * indent }<Icon>\n"
855
+ k << kml_array([
856
+ [@href, 'href', true],
857
+ [@x, 'gx:x', true],
858
+ [@y, 'gx:y', true],
859
+ [@w, 'gx:w', true],
860
+ [@h, 'gx:h', true],
861
+ [@refreshMode, 'refreshMode', true],
862
+ [@refreshInterval, 'refreshInterval', true],
863
+ [@viewRefreshMode, 'viewRefreshMode', true],
864
+ [@viewBoundScale, 'viewBoundScale', true],
865
+ [@viewFormat, 'viewFormat', true],
866
+ [@httpQuery, 'httpQuery', true],
867
+ ], indent + 4)
868
+ k << "#{ ' ' * indent }</Icon>\n"
869
+ end
870
+ end
871
+
872
+ # Corresponds to KML's IconStyle object.
873
+ class IconStyle < ColorStyle
874
+ attr_accessor :scale, :heading, :hotspot, :icon
875
+
876
+ def initialize(href, scale = 1, heading = 0, hs_x = 0.5, hs_y = 0.5, hs_xunits = :fraction, hs_yunits = :fraction, color = 'ffffffff', colormode = :normal)
877
+ super(color, colormode)
878
+ @scale = scale
879
+ @heading = heading
880
+ @icon = Icon.new(href) unless href.nil?
881
+ @hotspot = KMLxy.new(hs_x, hs_y, hs_xunits, hs_yunits) unless (hs_x.nil? and hs_y.nil? and hs_xunits.nil? and hs_yunits.nil?)
882
+ end
883
+
884
+ def to_kml(indent = 0)
885
+ k = <<-iconstyle1
886
+ #{ ' ' * indent }<IconStyle id="#{@id}">
887
+ #{ super(indent + 4) }
888
+ iconstyle1
889
+ k << "#{ ' ' * indent } <scale>#{@scale}</scale>\n" unless @scale.nil?
890
+ k << "#{ ' ' * indent } <heading>#{@heading}</heading>\n" unless @heading.nil?
891
+ k << @icon.to_kml(indent + 4) unless @icon.nil?
892
+ k << "#{ ' ' * indent } <hotSpot x=\"#{@hotspot.x}\" y=\"#{@hotspot.y}\" xunits=\"#{@hotspot.xunits}\" yunits=\"#{@hotspot.yunits}\" />\n" unless @hotspot.nil?
893
+ k << "#{ ' ' * indent }</IconStyle>\n"
894
+ end
895
+ end
896
+
897
+ # Corresponds to KML's LabelStyle object
898
+ class LabelStyle < ColorStyle
899
+ attr_accessor :scale
900
+
901
+ def initialize(scale = 1, color = 'ffffffff', colormode = :normal)
902
+ super(color, colormode)
903
+ @scale = scale
904
+ end
905
+
906
+ def to_kml(indent = 0)
907
+
908
+ <<-labelstyle
909
+ #{ ' ' * indent }<LabelStyle id="#{@id}">
910
+ #{ super(indent + 4) }
911
+ #{ ' ' * indent } <scale>#{@scale}</scale>
912
+ #{ ' ' * indent }</LabelStyle>
913
+ labelstyle
914
+ end
915
+ end
916
+
917
+ # Corresponds to KML's LineStyle object. Color is stored as an 8-character hex
918
+ # string, with two characters each of alpha, blue, green, and red values, in
919
+ # that order, matching the ordering the KML spec demands.
920
+ class LineStyle < ColorStyle
921
+ attr_accessor :outercolor, :outerwidth, :physicalwidth, :width
922
+
923
+ def initialize(width = 1, outercolor = 'ffffffff', outerwidth = 0, physicalwidth = 0, color = 'ffffffff', colormode = :normal)
924
+ super(color, colormode)
925
+ @width = width
926
+ @outercolor = outercolor
927
+ @outerwidth = outerwidth
928
+ @physicalwidth = physicalwidth
929
+ end
930
+
931
+ def to_kml(indent = 0)
932
+
933
+ <<-linestyle
934
+ #{ ' ' * indent }<LineStyle id="#{@id}">
935
+ #{ super(indent + 4) }
936
+ #{ ' ' * indent } <width>#{@width}</width>
937
+ #{ ' ' * indent } <gx:outerColor>#{@outercolor}</gx:outerColor>
938
+ #{ ' ' * indent } <gx:outerWidth>#{@outerwidth}</gx:outerWidth>
939
+ #{ ' ' * indent } <gx:physicalWidth>#{@physicalwidth}</gx:physicalWidth>
940
+ #{ ' ' * indent }</LineStyle>
941
+ linestyle
942
+ end
943
+ end
944
+
945
+ # Corresponds to KML's ListStyle object. Color is stored as an 8-character hex
946
+ # string, with two characters each of alpha, blue, green, and red values, in
947
+ # that order, matching the ordering the KML spec demands.
948
+ class ListStyle < ColorStyle
949
+ attr_accessor :listitemtype, :bgcolor, :state, :href
950
+
951
+ def initialize(bgcolor = nil, state = nil, href = nil, listitemtype = nil)
952
+ super(nil, :normal)
953
+ @bgcolor = bgcolor
954
+ @state = state
955
+ @href = href
956
+ @listitemtype = listitemtype
957
+ end
958
+
959
+ def to_kml(indent = 0)
960
+ k = "#{ ' ' * indent }<ListStyle id=\"#{@id}\">\n"
961
+ k << kml_array([
962
+ [@listitemtype, 'listItemType', true],
963
+ [@bgcolor, 'bgColor', true]
964
+ ], indent + 4)
965
+ if (! @state.nil? or ! @href.nil?) then
966
+ k << "#{ ' ' * indent } <ItemIcon>\n"
967
+ k << "#{ ' ' * indent } <state>#{@state}</state>\n" unless @state.nil?
968
+ k << "#{ ' ' * indent } <href>#{@href}</href>\n" unless @href.nil?
969
+ k << "#{ ' ' * indent } </ItemIcon>\n"
970
+ end
971
+ k << "#{ ' ' * indent }</ListStyle>\n"
972
+ k
973
+ end
974
+ end
975
+
976
+ # Corresponds to KML's PolyStyle object. Color is stored as an 8-character hex
977
+ # string, with two characters each of alpha, blue, green, and red values, in
978
+ # that order, matching the ordering the KML spec demands.
979
+ class PolyStyle < ColorStyle
980
+ attr_accessor :fill, :outline
981
+
982
+ def initialize(fill = 1, outline = 1, color = 'ffffffff', colormode = :normal)
983
+ super(color, colormode)
984
+ @fill = fill
985
+ @outline = outline
986
+ end
987
+
988
+ def to_kml(indent = 0)
989
+
990
+ k = <<-polystyle
991
+ #{ ' ' * indent }<PolyStyle id="#{@id}">
992
+ #{ super(indent + 4) }
993
+ #{ ' ' * indent } <fill>#{@fill}</fill>
994
+ polystyle
995
+ k << "#{ ' ' * indent } <outline>#{@outline}</outline>\n" unless @outline.nil?
996
+ k << "#{ ' ' * indent }</PolyStyle>\n"
997
+ k
998
+ end
999
+ end
1000
+
1001
+ # Abstract class corresponding to KML's StyleSelector object.
1002
+ class StyleSelector < KMLObject
1003
+ attr_accessor :attached
1004
+ def initialize
1005
+ super
1006
+ @attached = false
1007
+ Document.instance.styles << self
1008
+ end
1009
+
1010
+ def attached?
1011
+ @attached
1012
+ end
1013
+
1014
+ def attach(obj)
1015
+ @attached = true
1016
+ obj.styles << self
1017
+ end
1018
+ end
1019
+
1020
+ # Corresponds to KML's Style object. Attributes are expected to be IconStyle,
1021
+ # LabelStyle, LineStyle, PolyStyle, BalloonStyle, and ListStyle objects.
1022
+ class Style < StyleSelector
1023
+ attr_accessor :icon, :label, :line, :poly, :balloon, :list
1024
+ def initialize(icon = nil, label = nil, line = nil, poly = nil, balloon = nil, list = nil)
1025
+ super()
1026
+ @icon = icon
1027
+ @label = label
1028
+ @line = line
1029
+ @poly = poly
1030
+ @balloon = balloon
1031
+ @list = list
1032
+ end
1033
+
1034
+ def to_kml(indent = 0)
1035
+ k = ''
1036
+ k << super + "#{ ' ' * indent }<Style id=\"#{@id}\">\n"
1037
+ k << @icon.to_kml(indent + 4) unless @icon.nil?
1038
+ k << @label.to_kml(indent + 4) unless @label.nil?
1039
+ k << @line.to_kml(indent + 4) unless @line.nil?
1040
+ k << @poly.to_kml(indent + 4) unless @poly.nil?
1041
+ k << @balloon.to_kml(indent + 4) unless @balloon.nil?
1042
+ k << @list.to_kml(indent + 4) unless @list.nil?
1043
+ k << "#{ ' ' * indent }</Style>\n"
1044
+ k
1045
+ end
1046
+ end
1047
+
1048
+ # Corresponds to KML's StyleMap object.
1049
+ class StyleMap < StyleSelector
1050
+ # StyleMap manages pairs. The first entry in each pair is a string key, the
1051
+ # second is either a Style or a styleUrl. It will be assumed to be the
1052
+ # latter if its kind_of? method doesn't claim it's a Style object
1053
+ def initialize(pairs = {})
1054
+ super()
1055
+ @pairs = pairs
1056
+ end
1057
+
1058
+ # Adds a new Style to the StyleMap.
1059
+ def merge(a)
1060
+ @pairs.merge(a)
1061
+ end
1062
+
1063
+ def to_kml(indent = 0)
1064
+ t = super + "#{ ' ' * indent }<StyleMap id=\"#{@id}\">\n"
1065
+ @pairs.each do |k, v|
1066
+ t << "#{ ' ' * indent } <Pair>\n"
1067
+ t << "#{ ' ' * indent } <key>#{ k }</key>\n"
1068
+ if v.kind_of? Style then
1069
+ t << ( ' ' * indent ) << v.to_kml(indent + 8)
1070
+ else
1071
+ t << "#{ ' ' * indent } <styleUrl>#{ v }</styleUrl>\n"
1072
+ end
1073
+ t << "#{ ' ' * indent } </Pair>\n"
1074
+ end
1075
+ t << "#{ ' ' * indent }</StyleMap>\n"
1076
+ t
1077
+ end
1078
+ end
1079
+
1080
+ # Corresponds to KML's Placemark objects. The geometry attribute requires a
1081
+ # descendant of Geometry
1082
+ class Placemark < Feature
1083
+ attr_accessor :name, :geometry
1084
+ def initialize(name = nil, geo = nil)
1085
+ super(name)
1086
+ Document.instance.folder << self
1087
+ if geo.respond_to? '[]' then
1088
+ @geometry = geo
1089
+ else
1090
+ @geometry = [ geo ]
1091
+ end
1092
+ end
1093
+
1094
+ def to_kml(indent = 0)
1095
+ a = "#{ ' ' * indent }<Placemark id=\"#{ @id }\">\n"
1096
+ a << super(indent + 4) {
1097
+ k = ''
1098
+ @geometry.each do |i| k << i.to_kml(indent + 4) unless i.nil? end
1099
+ k
1100
+ }
1101
+ a << "#{ ' ' * indent }</Placemark>\n"
1102
+ end
1103
+
1104
+ def to_s
1105
+ "Placemark id #{ @id } named #{ @name }"
1106
+ end
1107
+
1108
+ def longitude
1109
+ @geometry.longitude
1110
+ end
1111
+
1112
+ def latitude
1113
+ @geometry.latitude
1114
+ end
1115
+
1116
+ def altitude
1117
+ @geometry.altitude
1118
+ end
1119
+
1120
+ def altitudeMode
1121
+ @geometry.altitudeMode
1122
+ end
1123
+
1124
+ def point
1125
+ if @geometry[0].kind_of? KMLPoint then
1126
+ @geometry[0]
1127
+ else
1128
+ raise "This placemark uses a non-point geometry, but the operation you're trying requires a point object"
1129
+ end
1130
+ end
1131
+ end
1132
+
1133
+ # Abstract class corresponding to KML's gx:TourPrimitive object. Tours are made up
1134
+ # of descendants of these.
1135
+ class TourPrimitive < KMLObject
1136
+ def initialize
1137
+ Document.instance.tour << self
1138
+ end
1139
+ end
1140
+
1141
+ # Cooresponds to KML's gx:FlyTo object. The @view parameter needs to look like an
1142
+ # AbstractView object
1143
+ class FlyTo < TourPrimitive
1144
+ attr_accessor :duration, :mode, :view
1145
+
1146
+ def initialize(view = nil, range = nil, duration = 0, mode = :bounce)
1147
+ @duration = duration
1148
+ @mode = mode
1149
+ if view.kind_of? AbstractView then
1150
+ @view = view
1151
+ else
1152
+ @view = LookAt.new(view)
1153
+ end
1154
+ if view.respond_to? 'range' and not range.nil? then
1155
+ @view.range = range
1156
+ end
1157
+ super()
1158
+ end
1159
+
1160
+ def to_kml(indent = 0)
1161
+ k = super + "#{ ' ' * indent }<gx:FlyTo>\n"
1162
+ k << kml_array([
1163
+ [ @duration, 'gx:duration', true ],
1164
+ [ @mode, 'gx:flyToMode', true ]
1165
+ ], indent + 4)
1166
+ k << @view.to_kml(indent + 4) unless @view.nil?
1167
+ k << "#{ ' ' * indent }</gx:FlyTo>\n"
1168
+ end
1169
+ end
1170
+
1171
+ # Corresponds to KML's gx:AnimatedUpdate object. For now at least, this isn't very
1172
+ # intelligent; you've got to manually craft the <Change> tag(s) within the
1173
+ # object.
1174
+ class AnimatedUpdate < TourPrimitive
1175
+ # For now, the user has to specify the change / create / delete elements in
1176
+ # the <Update> manually, rather than creating objects.
1177
+ attr_accessor :target, :delayedstart, :updates, :duration
1178
+
1179
+ # The updates argument is an array of strings containing <Change> elements
1180
+ def initialize(updates = [], duration = 0, target = '', delayedstart = nil)
1181
+ super()
1182
+ begin
1183
+ raise "incorrect object type" unless @target.kind_of? KMLObject
1184
+ @target = target.id
1185
+ rescue RuntimeError
1186
+ @target = target
1187
+ end
1188
+ @updates = updates
1189
+ @duration = duration
1190
+ @delayedstart = delayedstart
1191
+ end
1192
+
1193
+ # Adds another update string, presumably containing a <Change> element
1194
+ def <<(a)
1195
+ @updates << a << "\n"
1196
+ end
1197
+
1198
+ def to_kml(indent = 0)
1199
+ k = super + <<-animatedupdate_kml
1200
+ #{ ' ' * indent }<gx:AnimatedUpdate>
1201
+ #{ ' ' * indent } <gx:duration>#{@duration}</gx:duration>
1202
+ animatedupdate_kml
1203
+ k << "#{ ' ' * indent } <gx:delayeStart>#{@delayedstart}</gx:delayedStart>\n" unless @delayedstart.nil?
1204
+ k << "#{ ' ' * indent } <Update>\n"
1205
+ k << "#{ ' ' * indent } <targetHref>#{@target}</targetHref>\n"
1206
+ k << "#{ ' ' * indent } " << @updates.join("\n#{ ' ' * (indent + 1) }")
1207
+ k << "#{ ' ' * indent } </Update>\n#{ ' ' * indent }</gx:AnimatedUpdate>\n"
1208
+ k
1209
+ end
1210
+ end
1211
+
1212
+ # Corresponds to a KML gx:TourControl object
1213
+ class TourControl < TourPrimitive
1214
+ def initialize
1215
+ super
1216
+ end
1217
+
1218
+ def to_kml(indent = 0)
1219
+ k = "#{ ' ' * indent }<gx:TourControl id=\"#{ @id }\">\n"
1220
+ k << "#{ ' ' * indent } <gx:playMode>pause</gx:playMode>\n"
1221
+ k << "#{ ' ' * indent }</gx:TourControl>\n"
1222
+ end
1223
+ end
1224
+
1225
+ # Corresponds to a KML gx:Wait object
1226
+ class Wait < TourPrimitive
1227
+ attr_accessor :duration
1228
+ def initialize(duration = 0)
1229
+ super()
1230
+ @duration = duration
1231
+ end
1232
+
1233
+ def to_kml(indent = 0)
1234
+ super + <<-wait_kml
1235
+ #{ ' ' * indent }<gx:Wait><gx:duration>#{@duration}</gx:duration></gx:Wait>
1236
+ wait_kml
1237
+ end
1238
+ end
1239
+
1240
+ # Corresponds to a KML gx:SoundCue object
1241
+ class SoundCue < TourPrimitive
1242
+ attr_accessor :href, :delayedStart
1243
+ def initialize(href, delayedStart = nil)
1244
+ super()
1245
+ @href = href
1246
+ @delayedStart = delayedStart
1247
+ end
1248
+
1249
+ def to_kml(indent = 0)
1250
+ k = "#{ ' ' * indent }<gx:SoundCue id=\"#{ @id }\">\n"
1251
+ k << "#{ ' ' * indent } <href>#{ @href }</href>\n"
1252
+ k << "#{ ' ' * indent } <gx:delayedStart>#{ @delayedStart }</gx:delayedStart>\n" unless @delayedStart.nil?
1253
+ k << "#{ ' ' * indent}</gx:SoundCue>\n"
1254
+ end
1255
+ end
1256
+
1257
+ # Corresponds to a KML gx:Tour object
1258
+ class Tour < KMLObject
1259
+ attr_accessor :name, :description, :last_abs_view
1260
+ def initialize(name = nil, description = nil)
1261
+ super()
1262
+ @name = name
1263
+ @description = description
1264
+ @items = []
1265
+ end
1266
+
1267
+ # Add another element to this Tour
1268
+ def <<(a)
1269
+ @items << a
1270
+ @last_abs_view = a.view if a.kind_of? FlyTo
1271
+ end
1272
+
1273
+ def to_kml(indent = 0)
1274
+ k = super + "#{ ' ' * indent }<gx:Tour id=\"#{ @id }\">\n"
1275
+ k << kml_array([
1276
+ [ @name, 'name', true ],
1277
+ [ @description, 'description', true ],
1278
+ ], indent + 4)
1279
+ k << "#{ ' ' * indent } <gx:Playlist>\n";
1280
+
1281
+ @items.map do |a| k << a.to_kml(indent + 8) << "\n" end
1282
+
1283
+ k << "#{ ' ' * indent } </gx:Playlist>\n"
1284
+ k << "#{ ' ' * indent }</gx:Tour>\n"
1285
+ k
1286
+ end
1287
+ end
1288
+
1289
+ # Abstract class corresponding to the KML Overlay object
1290
+ class Overlay < Feature
1291
+ attr_accessor :color, :drawOrder, :icon
1292
+
1293
+ def initialize(icon, name = nil)
1294
+ super(name)
1295
+ if icon.respond_to?('to_kml') then
1296
+ @icon = icon
1297
+ elsif not icon.nil?
1298
+ @icon = Icon.new(icon.to_s)
1299
+ end
1300
+ Document.instance.folder << self
1301
+ end
1302
+
1303
+ def to_kml(indent = 0)
1304
+ k = super(indent) + kml_array([
1305
+ [ @color, 'color', true ],
1306
+ [ @drawOrder, 'drawOrder', true ],
1307
+ ], indent + 4)
1308
+ k << @icon.to_kml(indent) unless @icon.nil?
1309
+ k
1310
+ end
1311
+ end
1312
+
1313
+ # Corresponds to KML's ScreenOverlay object
1314
+ class ScreenOverlay < Overlay
1315
+ attr_accessor :overlayXY, :screenXY, :rotationXY, :size, :rotation
1316
+ def initialize(icon, name = nil, size = nil, rotation = nil, overlayXY = nil, screenXY = nil, rotationXY = nil)
1317
+ super(icon, name)
1318
+ @overlayXY = overlayXY
1319
+ @screenXY = screenXY
1320
+ @rotationXY = rotationXY
1321
+ @size = size
1322
+ @rotation = rotation
1323
+ end
1324
+
1325
+ def to_kml(indent = 0)
1326
+ k = "#{ ' ' * indent }<ScreenOverlay id=\"#{ @id }\">\n"
1327
+ k << super(indent + 4)
1328
+ k << @overlayXY.to_kml('overlayXY', indent + 4) unless @overlayXY.nil?
1329
+ k << @screenXY.to_kml('screenXY', indent + 4) unless @screenXY.nil?
1330
+ k << @rotationXY.to_kml('rotationXY', indent + 4) unless @rotationXY.nil?
1331
+ k << @size.to_kml('size', indent + 4) unless @size.nil?
1332
+ k << "#{ ' ' * indent } <rotation>#{ @rotation }</rotation>\n" unless @rotation.nil?
1333
+ k << "#{ ' ' * indent }</ScreenOverlay>\n"
1334
+ end
1335
+ end
1336
+
1337
+ # Supporting object for the PhotoOverlay class
1338
+ class ViewVolume
1339
+ attr_accessor :leftFov, :rightFov, :bottomFov, :topFov, :near
1340
+ def initialize(near, leftFov = -45, rightFov = 45, bottomFov = -45, topFov = 45)
1341
+ @leftFov = leftFov
1342
+ @rightFov = rightFov
1343
+ @bottomFov = bottomFov
1344
+ @topFov = topFov
1345
+ @near = near
1346
+ end
1347
+
1348
+ def to_kml(indent = 0)
1349
+
1350
+ <<-viewvolume
1351
+ #{ ' ' * indent }<ViewVolume>
1352
+ #{ ' ' * indent } <near>#{@near}</near>
1353
+ #{ ' ' * indent } <leftFov>#{@leftFov}</leftFov>
1354
+ #{ ' ' * indent } <rightFov>#{@rightFov}</rightFov>
1355
+ #{ ' ' * indent } <bottomFov>#{@bottomFov}</bottomFov>
1356
+ #{ ' ' * indent } <topFov>#{@topFov}</topFov>
1357
+ #{ ' ' * indent }</ViewVolume>
1358
+ viewvolume
1359
+ end
1360
+ end
1361
+
1362
+ # Supporting object for the PhotoOverlay class
1363
+ class ImagePyramid
1364
+ attr_accessor :tileSize, :maxWidth, :maxHeight, :gridOrigin
1365
+
1366
+ def initialize(maxWidth, maxHeight, gridOrigin, tileSize = 256)
1367
+ @tileSize = tileSize
1368
+ @maxWidth = maxWidth
1369
+ @maxHeight = maxHeight
1370
+ @gridOrigin = gridOrigin
1371
+ end
1372
+
1373
+ def to_kml(indent = 0)
1374
+
1375
+ <<-imagepyramid
1376
+ #{ ' ' * indent }<ImagePyramid>
1377
+ #{ ' ' * indent } <tileSize>#{@tileSize}</tileSize>
1378
+ #{ ' ' * indent } <maxWidth>#{@maxWidth}</maxWidth>
1379
+ #{ ' ' * indent } <maxHeight>#{@maxHeight}</maxHeight>
1380
+ #{ ' ' * indent } <gridOrigin>#{@gridOrigin}</gridOrigin>
1381
+ #{ ' ' * indent }</ImagePyramid>
1382
+ imagepyramid
1383
+ end
1384
+ end
1385
+
1386
+ # Corresponds to KML's PhotoOverlay class
1387
+ class PhotoOverlay < Overlay
1388
+ attr_accessor :rotation, :viewvolume, :imagepyramid, :point, :shape
1389
+
1390
+ def initialize(icon, point, rotation = 0, viewvolume = nil, imagepyramid = nil, shape = :rectangle)
1391
+ super(icon)
1392
+ if point.respond_to?('point')
1393
+ @point = point.point
1394
+ else
1395
+ @point = point
1396
+ end
1397
+ @rotation = rotation
1398
+ @viewVolume = viewvolume
1399
+ @imagePyramid = imagepyramid
1400
+ @shape = shape
1401
+ end
1402
+
1403
+ def to_kml(indent = 0)
1404
+ k = "#{ ' ' * indent }<PhotoOverlay>\n"
1405
+ k << super(indent + 4)
1406
+ k << @viewVolume.to_kml(indent + 4) unless @viewVolume.nil?
1407
+ k << @imagePyramid.to_kml(indent + 4) unless @imagePyramid.nil?
1408
+ k << @point.to_kml(indent + 4, true)
1409
+ k << "#{ ' ' * indent } <rotation>#{ @rotation }</rotation>\n"
1410
+ k << "#{ ' ' * indent } <shape>#{ @shape }</shape>\n"
1411
+ k << "#{ ' ' * indent }</PhotoOverlay>\n"
1412
+ end
1413
+ end
1414
+
1415
+ # Corresponds to KML's LatLonBox and LatLonAltBox
1416
+ class LatLonBox
1417
+ attr_reader :north, :south, :east, :west
1418
+ attr_accessor :rotation, :minAltitude, :maxAltitude, :altitudeMode
1419
+
1420
+ def initialize(north, south, east, west, rotation = 0, minAltitude = nil, maxAltitude = nil, altitudeMode = :clampToGround)
1421
+ @north = convert_coord north
1422
+ @south = convert_coord south
1423
+ @east = convert_coord east
1424
+ @west = convert_coord west
1425
+ @minAltitude = minAltitude
1426
+ @maxAltitude = maxAltitude
1427
+ @altitudeMode = altitudeMode
1428
+ @rotation = rotation
1429
+ end
1430
+
1431
+ def north=(a)
1432
+ @north = convert_coord a
1433
+ end
1434
+
1435
+ def south=(a)
1436
+ @south = convert_coord a
1437
+ end
1438
+
1439
+ def east=(a)
1440
+ @east = convert_coord a
1441
+ end
1442
+
1443
+ def west=(a)
1444
+ @west = convert_coord a
1445
+ end
1446
+
1447
+ def to_kml(indent = 0, alt = false)
1448
+ name = alt ? 'LatLonAltBox' : 'LatLonBox'
1449
+ k = <<-latlonbox
1450
+ #{ ' ' * indent }<#{ name }>
1451
+ #{ ' ' * indent } <north>#{ @north }</north>
1452
+ #{ ' ' * indent } <south>#{ @south }</south>
1453
+ #{ ' ' * indent } <east>#{ @east }</east>
1454
+ #{ ' ' * indent } <west>#{ @west }</west>
1455
+ latlonbox
1456
+ k << "#{ ' ' * indent } <minAltitude>#{ @minAltitude }</minAltitude>\n" unless @minAltitude.nil?
1457
+ k << "#{ ' ' * indent } <maxAltitude>#{ @maxAltitude }</maxAltitude>\n" unless @maxAltitude.nil?
1458
+ if (not @minAltitude.nil? or not @maxAltitude.nil?) then
1459
+ if @altitudeMode == :clampToGround or @altitudeMode == :relativeToGround or @altitudeMode == :absolute then
1460
+ altitudeModeString = "#{ ' ' * indent } <altitudeMode>#{ @altitudeMode }</altitudeMode>\n"
1461
+ else
1462
+ altitudeModeString = "#{ ' ' * indent } <gx:altitudeMode>#{ @altitudeMode }</gx:altitudeMode>\n"
1463
+ end
1464
+ end
1465
+ k << <<-latlonbox2
1466
+ #{ ' ' * indent } <rotation>#{ @rotation }</rotation>
1467
+ #{ ' ' * indent }</#{ name }>
1468
+ latlonbox2
1469
+ end
1470
+ end
1471
+
1472
+ # Corresponds to KML's gx:LatLonQuad object
1473
+ class LatLonQuad
1474
+ attr_accessor :lowerLeft, :lowerRight, :upperRight, :upperLeft
1475
+ def initialize(lowerLeft, lowerRight, upperRight, upperLeft)
1476
+ @lowerLeft = lowerLeft
1477
+ @lowerRight = lowerRight
1478
+ @upperRight = upperRight
1479
+ @upperLeft = upperLeft
1480
+ end
1481
+
1482
+ def to_kml(indent = 0)
1483
+
1484
+ <<-latlonquad
1485
+ #{ ' ' * indent }<gx:LatLonQuad>
1486
+ #{ ' ' * indent } <coordinates>#{ @lowerLeft.longitude },#{ @lowerLeft.latitude } #{ @lowerRight.longitude },#{ @lowerRight.latitude } #{ @upperRight.longitude },#{ @upperRight.latitude } #{ @upperLeft.longitude },#{ @upperLeft.latitude }</coordinates>
1487
+ #{ ' ' * indent }</gx:LatLonQuad>
1488
+ latlonquad
1489
+ end
1490
+ end
1491
+
1492
+ # Corresponds to KML's GroundOverlay object
1493
+ class GroundOverlay < Overlay
1494
+ attr_accessor :altitude, :altitudeMode, :latlonbox, :latlonquad
1495
+ def initialize(icon, latlonbox = nil, latlonquad = nil, altitude = 0, altitudeMode = :clampToGround)
1496
+ super(icon)
1497
+ @latlonbox = latlonbox
1498
+ @latlonquad = latlonquad
1499
+ @altitude = altitude
1500
+ @altitudeMode = altitudeMode
1501
+ end
1502
+
1503
+ def to_kml(indent = 0)
1504
+ raise "Either latlonbox or latlonquad must be non-nil" if @latlonbox.nil? and @latlonquad.nil?
1505
+
1506
+ k = "#{ ' ' * indent}<GroundOverlay id=\"#{ @id }\">\n"
1507
+ k << super(indent + 4)
1508
+ k << "#{ ' ' * indent } <altitude>#{ @altitude }</altitude>\n"
1509
+ k << ' ' * indent
1510
+ if @altitudeMode == :clampToGround or @altitudeMode == :relativeToGround or @altitudeMode == :absolute then
1511
+ k << "#{ ' ' * indent } <altitudeMode>#{ @altitudeMode }</altitudeMode>\n"
1512
+ else
1513
+ k << "#{ ' ' * indent } <gx:altitudeMode>#{ @altitudeMode }</gx:altitudeMode>\n"
1514
+ end
1515
+ k << @latlonbox.to_kml(indent + 4) unless @latlonbox.nil?
1516
+ k << @latlonquad.to_kml(indent + 4) unless @latlonquad.nil?
1517
+ k << "#{ ' ' * indent }</GroundOverlay>\n"
1518
+ k
1519
+ end
1520
+ end
1521
+
1522
+ # Corresponds to the LOD (Level of Detail) object
1523
+ class Lod
1524
+ attr_accessor :minpixels, :maxpixels, :minfade, :maxfade
1525
+ def initialize(minpixels, maxpixels, minfade, maxfade)
1526
+ @minpixels = minpixels
1527
+ @maxpixels = maxpixels
1528
+ @minfade = minfade
1529
+ @maxfade = maxfade
1530
+ end
1531
+
1532
+ def to_kml(indent = 0)
1533
+
1534
+ <<-lod
1535
+ #{ ' ' * indent }<Lod>
1536
+ #{ ' ' * indent } <minLodPixels>#{ @minpixels }</minLodPixels>
1537
+ #{ ' ' * indent } <maxLodPixels>#{ @maxpixels }</maxLodPixels>
1538
+ #{ ' ' * indent } <minFadeExtent>#{ @minfade }</minFadeExtent>
1539
+ #{ ' ' * indent } <maxFadeExtent>#{ @maxfade }</maxFadeExtent>
1540
+ #{ ' ' * indent }</Lod>
1541
+ lod
1542
+ end
1543
+ end
1544
+
1545
+ # Corresponds to the KML Region object
1546
+ class Region < KMLObject
1547
+ attr_accessor :latlonaltbox, :lod
1548
+
1549
+ def initialize(latlonaltbox, lod)
1550
+ super()
1551
+ @latlonaltbox = latlonaltbox
1552
+ @lod = lod
1553
+ end
1554
+
1555
+ def to_kml(indent = 0)
1556
+ k = "#{' ' * indent}<Region id=\"#{@id}\">\n"
1557
+ k << @latlonaltbox.to_kml(indent + 4, true) unless @latlonaltbox.nil?
1558
+ k << @lod.to_kml(indent + 4) unless @lod.nil?
1559
+ k << "#{' ' * indent}</Region>\n"
1560
+ k
1561
+ end
1562
+ end
1563
+
1564
+ # Sub-object in the KML Model class
1565
+ class Orientation
1566
+ attr_accessor :heading, :tilt, :roll
1567
+ def initialize(heading, tilt, roll)
1568
+ @heading = heading
1569
+ # Although the KML reference by Google is clear on these ranges, Google Earth
1570
+ # supports values outside the ranges, and sometimes it's useful to use
1571
+ # them. So I'm turning off this error checking
1572
+ # raise "Heading should be between 0 and 360 inclusive; you gave #{ heading }" unless @heading <= 360 and @heading >= 0
1573
+ @tilt = tilt
1574
+ # raise "Tilt should be between 0 and 180 inclusive; you gave #{ tilt }" unless @tilt <= 180 and @tilt >= 0
1575
+ @roll = roll
1576
+ # raise "Roll should be between 0 and 180 inclusive; you gave #{ roll }" unless @roll <= 180 and @roll >= 0
1577
+ end
1578
+
1579
+ def to_kml(indent = 0)
1580
+ k = "#{ ' ' * indent }<Orientation>\n"
1581
+ k << "#{ ' ' * indent } <heading>#{ @heading }</heading>\n"
1582
+ k << "#{ ' ' * indent } <tilt>#{ @tilt }</tilt>\n"
1583
+ k << "#{ ' ' * indent } <roll>#{ @roll }</roll>\n"
1584
+ k << "#{ ' ' * indent }</Orientation>\n"
1585
+ k
1586
+ end
1587
+ end
1588
+
1589
+ # Sub-object in the KML Model class
1590
+ class Scale
1591
+ attr_accessor :x, :y, :z
1592
+ def initialize(x, y, z = 1)
1593
+ @x = x
1594
+ @y = y
1595
+ @z = z
1596
+ end
1597
+
1598
+ def to_kml(indent = 0)
1599
+ k = "#{ ' ' * indent }<Scale>\n"
1600
+ k << "#{ ' ' * indent } <x>#{ x }</x>\n"
1601
+ k << "#{ ' ' * indent } <y>#{ y }</y>\n"
1602
+ k << "#{ ' ' * indent } <z>#{ z }</z>\n"
1603
+ k << "#{ ' ' * indent }</Scale>\n"
1604
+ end
1605
+ end
1606
+
1607
+ # Sub-object in the KML ResourceMap class
1608
+ class Alias
1609
+ attr_accessor :targetHref, :sourceHref
1610
+ def initialize(targetHref = nil, sourceHref = nil)
1611
+ @targetHref = targetHref
1612
+ @sourceHref = sourceHref
1613
+ end
1614
+
1615
+ def to_kml(indent = 0)
1616
+ k = "#{ ' ' * indent }<Alias>\n"
1617
+ k << "#{ ' ' * indent } <targetHref>#{ @targetHref }</targetHref>\n"
1618
+ k << "#{ ' ' * indent } <sourceHref>#{ @sourceHref }</sourceHref>\n"
1619
+ k << "#{ ' ' * indent }</Alias>\n"
1620
+ k
1621
+ end
1622
+ end
1623
+
1624
+ # Sub-object in the KML Model class
1625
+ class ResourceMap
1626
+ attr_accessor :aliases
1627
+ def initialize(aliases = [])
1628
+ @aliases = []
1629
+ if not aliases.nil? then
1630
+ if aliases.kind_of? Enumerable then
1631
+ @aliases += aliases
1632
+ else
1633
+ @aliases << aliases
1634
+ end
1635
+ end
1636
+ end
1637
+
1638
+ def to_kml(indent = 0)
1639
+ return '' if @aliases.size == 0
1640
+ k = "#{ ' ' * indent }<ResourceMap>\n"
1641
+ k << "#{ ' ' * indent }</ResourceMap>\n"
1642
+ @aliases.each do |a| k << a.to_kml(indent + 4) end
1643
+ k
1644
+ end
1645
+ end
1646
+
1647
+ # Corresponds to KML's Link object
1648
+ class Link < KMLObject
1649
+ attr_accessor :href, :refreshMode, :refreshInterval, :viewRefreshMode, :viewBoundScale, :viewFormat, :httpQuery
1650
+ def initialize(href = '', refreshMode = :onChange, viewRefreshMode = :never)
1651
+ super()
1652
+ @href = href
1653
+ @refreshMode = refreshMode
1654
+ @viewRefreshMode = viewRefreshMode
1655
+ end
1656
+
1657
+ def to_kml(indent = 0)
1658
+ k = "#{ ' ' * indent }<Link id=\"#{ @id }\">\n"
1659
+ k << "#{ ' ' * indent } <href>#{ @href }</href>\n"
1660
+ k << "#{ ' ' * indent } <refreshMode>#{ @refreshMode }</refreshMode>\n"
1661
+ k << "#{ ' ' * indent } <viewRefreshMode>#{ @viewRefreshMode }</viewRefreshMode>\n"
1662
+ k << "#{ ' ' * indent } <refreshInterval>#{ @refreshInterval }</refreshInterval>\n" unless @refreshInterval.nil?
1663
+ k << "#{ ' ' * indent } <viewBoundScale>#{ @viewBoundScale }</viewBoundScale>\n" unless @viewBoundScale.nil?
1664
+ k << "#{ ' ' * indent } <viewFormat>#{ @viewFormat }</viewFormat>\n" unless @viewFormat.nil?
1665
+ k << "#{ ' ' * indent } <httpQuery>#{ @httpQuery }</httpQuery>\n" unless @httpQuery.nil?
1666
+ k << "#{ ' ' * indent }</Link>\n"
1667
+ k
1668
+ end
1669
+ end
1670
+
1671
+ # Corresponds to the KML Model class
1672
+ class Model < Geometry
1673
+ attr_accessor :link, :location, :orientation, :scale, :resourceMap
1674
+
1675
+ # location should be a KMLPoint, or some object that can behave like one,
1676
+ # including a Placemark. Model will get its Location and altitudeMode data
1677
+ # from this attribute
1678
+ def initialize(link, location, orientation, scale, resourceMap)
1679
+ super()
1680
+ @link = link
1681
+ @location = location
1682
+ @orientation = orientation
1683
+ @scale = scale
1684
+ @resourceMap = resourceMap
1685
+ end
1686
+
1687
+ def to_kml(indent = 0)
1688
+ k = "#{ ' ' * indent }<Model id=\"#{ @id }\">\n"
1689
+ k << @link.to_kml(indent + 4)
1690
+ if @location.altitudeMode == :clampToGround or @location.altitudeMode == :relativeToGround or @location.altitudeMode == :absolute then
1691
+ k << "#{ ' ' * indent } <altitudeMode>#{ @location.altitudeMode }</altitudeMode>\n"
1692
+ else
1693
+ k << "#{ ' ' * indent } <gx:altitudeMode>#{ @location.altitudeMode }</gx:altitudeMode>\n"
1694
+ end
1695
+ k << "#{ ' ' * indent } <Location>\n"
1696
+ k << "#{ ' ' * indent } <longitude>#{ @location.longitude }</longitude>\n"
1697
+ k << "#{ ' ' * indent } <latitude>#{ @location.latitude }</latitude>\n"
1698
+ k << "#{ ' ' * indent } <altitude>#{ @location.altitude }</altitude>\n"
1699
+ k << "#{ ' ' * indent } </Location>\n"
1700
+ k << @orientation.to_kml(indent + 4)
1701
+ k << @scale.to_kml(indent + 4)
1702
+ k << @resourceMap.to_kml(indent + 4)
1703
+ k << "#{ ' ' * indent }</Model>\n"
1704
+ k
1705
+ end
1706
+ end