kamelopard 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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