source: DPPP/kml/csml2kml/python/csml2kml/csml2kml/KML.py @ 3653

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/DPPP/kml/csml2kml/python/csml2kml/csml2kml/KML.py@3653
Revision 3653, 18.4 KB checked in by mkochan, 11 years ago (diff)

Replaced "pylab" with "matplotlib" in imports in csml2kml, to make it work on superglue.

Line 
1'''
2Classes representing various KML elements.
3
4The official Google KML reference is located at
5U{http://code.google.com/apis/kml/documentation/kml_tags_21.html}.
6'''
7
8import os
9import sys
10from cElementTree import ElementTree, Element, SubElement, XML
11import zipfile
12from StringIO import StringIO
13
14class KMLElement:
15    '''
16    Abstract class, represents a KML element. Each C{KMLElement} can be I{built} using the C{build} method
17    into an actual C{cElement.Element} object, which is the actual XML.
18    '''
19
20    def build(self):
21        '''
22        Build this object, which is a representation of a KML element,
23        and all of its contained objects (if any), I{directly} into a XML element.
24        @rtype: C{cElement.Element}
25        '''
26        raise NotImplementedError("Abstract method, to be overriden by child classes")
27
28class KMLDocument(KMLElement):
29    '''
30    Wraps around a whole KML document and makes it possible to save it to a file directly.
31    Represent the I{<kml:Document>}, so that the method C{build} builds the kml:Document element,
32    but also contains the method C{save} that allows saving the XML to a file, wrapped within the <kml> header.
33    '''
34
35    def __init__(self, name, styles):
36
37        self.name = name
38        self.styles = styles
39        self.elements = []
40
41    def build(self):
42
43        # Create the <Document> an element to hold the document
44        documentElement = Element('Document')
45        SubElement(documentElement, 'name').text = self.name
46        SubElement(documentElement, 'open').text = '1'
47
48        # Build the associated styles
49        for style in self.styles:
50            documentElement.append( style.build() )
51
52        # Build the sub-elements
53        for element in self.elements:
54            documentElement.append( element.build() )
55       
56        return documentElement
57       
58    def save(self, outputFilename):
59        '''
60        Save the document to file C{outputFilename} (full path), wrapped in the I{<kml>} element.
61        @param outputFilename: Name of the destination file.
62        If the suffix is "kml" (in any case), the saved file will be an unpacked KML file.
63        If the suffix is "kmz" (in any case), the saved file will be a ZIP archive file,
64        containing the KML in a file named 'doc.kml'.
65        '''
66
67        def _indentXML(elem, level=0):
68            '''Auxiliary function, indents XML'''
69            i = "\n" + level * "  "
70            if len(elem):
71                if not elem.text or not elem.text.strip():
72                    elem.text = i + "  "
73                for child in elem:
74                    _indentXML(child, level+1)
75                if not child.tail or not child.tail.strip():
76                    child.tail = i
77                if not elem.tail or not elem.tail.strip():
78                    elem.tail = i
79            else:
80                if level and (not elem.tail or not elem.tail.strip()):
81                    elem.tail = i
82
83        def _save(file):
84            '''
85            Builds and saves the document in the KML format into a file.
86            @param file: An open file-like object to write to.
87            '''
88
89            # Build the Document element
90            documentElement = self.build()
91
92            # Attach the Document element as a subelement of a root 'kml' element
93            rootElement=Element('kml', xmlns='http://earth.google.com/kml/2.2')
94            rootElement.append(documentElement)
95            _indentXML(rootElement)
96
97            # Write the KML document to a file
98            elementTree = ElementTree(rootElement)       
99            elementTree.write(file)
100
101        def _saveKml(filename):
102            '''
103            Builds and saves the document in the KML format into a file named filename.
104            @param filename: Name of the KML output file.
105            '''
106            kmlFile = open(filename, 'w')
107            _save(kmlFile)
108            kmlFile.close()
109
110        def _saveKmz(filename):
111            '''
112            Builds and saves the document into a KMZ file, i.e. using KML format with ZIP compression.
113            @param filename: Name of the KMZ output file.
114            '''
115            # Write the KMZ archive
116            buf = StringIO()
117            _save(buf)
118            kmzFile = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED)
119            kmzFile.writestr('doc.kml', buf.getvalue())
120            kmzFile.close()
121            buf.close()
122
123        # Check the suffix of outputFilename, and depending on suffix, either save the document
124        # directly to a KML file, or save it packed in a KMZ file.
125        suffix = outputFilename[-3:].lower()
126        if suffix == 'kml':
127            _saveKml(outputFilename)
128        elif suffix == 'kmz':
129            #shortOutputFilename = outputFilename.split('/')[-1]          # separate out the short filename
130            #outputDir = outputFilename[:-(len(shortOutputFilename)+1)]   # separate out the directory name
131            #_save(outputDir + '/doc.kml')
132            #os.system('zip -q %s %s/doc.kml' % (outputFilename, outputDir))
133            #os.remove(outputDir + '/doc.kml')
134            _saveKmz(outputFilename)
135        else:
136            raise ValueError('Wrong file suffix, only "kml" and "kmz" allowed.')
137
138class KMLStyle(KMLElement):
139    '''
140    Represents the I{<kml:Style>} tag.
141
142    KML styles are used to extend behaviour of various KML elements. In particular, they can be used to define:
143      - images used for drawing KML placemarks
144      - defining HTML templates for placemark balloons
145      - defining list types for KML folders
146    Any KML element can have exactly one style associated with it.
147    @ivar id: Style identifier
148    @type id: C{str}
149    @ivar iconURL: Can contain URL for an icon image to be drawn on spot of a placemark;
150    to be associated with C{KMLPlacemark} objects.
151    @type iconURL: C{str}
152    @ivar balloonTemplate: Can contain a HTML template with variables that are substituted during build
153    using from C{KMLPlacemark.data} attributes;
154    to be associated with C{KMLPlacemark} objects.
155    @type balloonTemplate: C{str}
156    @ivar listItemType: Can contain a string designating listing mode for a KML folder;
157    to be associated with C{KMLFolder} objects;
158    permitted values are: "C{check}", "C{checkOffOnly}", "C{checkHideChildren}", "C{radioFolder}".
159    @type listItemType: C{str}
160    '''
161
162    def __init__(self, id, iconURL = None, balloonTemplate = None, listItemType = None):
163        self.id = id
164        self.iconURL = iconURL
165        self.balloonTemplate = balloonTemplate
166        if listItemType:
167            allowedValues = ['check', 'checkOffOnly', 'checkHideChildren', 'radioFolder']
168            if not listItemType in allowedValues:
169                raise ValueError('listItemType not among allowed values: ' + str(allowedValues))
170        self.listItemType = listItemType
171
172    def build(self):
173
174        styleElement = Element('Style')
175        styleElement.set('id', self.id)
176
177        # If specified, build the IconStyle element -- for assigning an icon to station placemarks
178        if self.iconURL:
179            iconStyleElement = SubElement(styleElement, 'IconStyle')
180            SubElement(iconStyleElement, 'scale').text = '1.2'
181            iconElement = SubElement(iconStyleElement, 'Icon')
182            SubElement(iconElement, 'href').text = self.iconURL
183
184        # If specified, build the BalloonStyle sub-element -- an HTML template for the balloons
185        if self.balloonTemplate:
186            balloonStyleElement = SubElement(styleElement, 'BalloonStyle')
187            SubElement(balloonStyleElement, 'text').text = self.balloonTemplate
188
189        # If specified, build the ListStyle sub-element -- which determines how the associated lists
190        # in the left-hand side of the Google Earth screen are going to display/behave
191        if self.listItemType:
192            listStyleElement = SubElement(styleElement, 'ListStyle')
193            SubElement(listStyleElement, 'listItemType').text = self.listItemType
194       
195        return styleElement
196
197def createDefaultPlacemarKMLStyle(id = 'default_placemark_style',
198                                  iconURL = 'http://maps.google.com/mapfiles/kml/shapes/target.png',
199                                  balloonTemplate = ''):
200    '''
201    A factory method defined for convenience. Creates a "default placemark style".
202    @rtype: C{KMLStyle}
203    '''
204    return KMLStyle(id, iconURL, balloonTemplate)
205
206class KMLPlacemark(KMLElement):
207    '''
208    Represents the I{<kml:Placemark>} tag.
209    @ivar id: Placemark identifier
210    @type id: C{str}
211    @ivar name: Human-readable name
212    @type name: C{str}
213    @ivar lon: Longitude
214    @type lon: C{float}
215    @ivar lat: Latitude
216    @type lat: C{float}
217    @ivar styleID: Style identifier (see C{KMLStyle.id})
218    @type styleID: C{str}
219    @ivar data: A dictionary containing instance-specific data items which are to be substituted
220    for in the balloon template by the values defined by C{self.data}.
221    @type data: C{dict}
222    @ivar visible: Determines whether the placemark is visible (ie. checked) initially when loaded
223    @type visible: C{bool}
224    '''
225   
226    def __init__(self, id, name, lon, lat, styleID = None, data=None, visible = True):
227        self.id = id
228        self.name = name
229        self.lon = lon
230        self.lat = lat
231        self.styleID = styleID
232        self.data = data
233        self.visible = visible
234
235    def build(self):
236        placemarkElement = Element('Placemark')
237        SubElement(placemarkElement, 'name').text = self.name
238
239        if self.visible:
240            SubElement(placemarkElement, 'visibility').text = '1'
241        else:
242            SubElement(placemarkElement, 'visibility').text = '0'
243
244        if self.styleID:
245            SubElement(placemarkElement, 'styleUrl').text = '#' + self.styleID
246
247        lookAtElement = SubElement(placemarkElement, 'LookAt')
248        SubElement(lookAtElement, 'longitude').text = str(self.lon)
249        SubElement(lookAtElement, 'latitude').text = str(self.lat)
250
251        pointElement = SubElement(placemarkElement, 'Point')
252        SubElement(pointElement, 'coordinates').text = '%f,%f,%f' % (self.lon, self.lat, 0.)
253
254        # If the "data" dictionary is provided, create the additional <ExtendedData> element,
255        # which contains specific data items, which will be automatically substituted
256        # for placemarks in the balloon template when the user views the document in Google Earth.
257        if self.data:
258            extendedDataElement = SubElement(placemarkElement, 'ExtendedData')
259            for key in self.data:
260                dataElement = SubElement(extendedDataElement, 'Data')
261                dataElement.set('name', key)
262                value = self.data[key]
263                value = value.replace('#ID#', self.id)
264                value = value.replace('#NAME#', self.name)
265                SubElement(dataElement, 'value').text = value
266               
267        return placemarkElement
268
269class KMLFolder(KMLElement):
270    '''
271    Represents the I{<kml:Folder>} tag.
272    @ivar name: Human-readable folder name
273    @type name: C{str}
274    @ivar description: Human-readable detailed description of folder contents (appears greyish below the name).
275    @type description: C{str}
276    @ivar children: A list of embedded elements.
277    @type children: C{KMLElement list}
278    @ivar styleID: Style identifier (see C{KMLStyle.id})
279    @type styleID: C{str}
280    @ivar region: A region associated with this folder.
281    @type region: C{KMLRegion}
282    @ivar opened: Determines whether the placemark is opened (that is, listed) when initially loaded
283    @type opened: C{bool}
284    @ivar visible: Determines whether the placemark is visible (that is, checked) when initially loaded
285    @type visible: C{bool}
286    '''
287
288    def __init__(self, name, children, styleID = None, region = None, opened = True, visible = True, description = None):
289        self.name = name
290        self.children = children
291        self.styleID = styleID
292        self.region = region
293        self.opened = opened
294        self.visible = visible
295        self.description = description
296
297    def build(self):
298        folderElement = Element('Folder')
299        if self.styleID:
300            SubElement(folderElement, 'styleUrl').text = '#' + self.styleID
301        SubElement(folderElement, 'name').text = self.name
302        if self.description:
303            SubElement(folderElement, 'description').text = self.description
304        if self.visible:
305            SubElement(folderElement, 'visibility').text = '1'
306        else:
307            SubElement(folderElement, 'visibility').text = '0'
308        if self.opened:
309            SubElement(folderElement, 'open').text = '1'
310        else:
311            SubElement(folderElement, 'open').text = '0'
312        if self.region:
313            if not isinstance(self.region, KMLRegion):
314                raise TypeError('Region not a KMLRegion')
315            folderElement.append( self.region.build() )
316        for child in self.children:
317            if not isinstance(child, KMLElement):
318                raise TypeError('Child does not have a KMLElement base class')
319            folderElement.append( child.build() )
320        return folderElement
321
322class KMLRegion(KMLElement):
323    '''
324    Represents the I{<kml:Region>} tag.
325
326    Elements which have a region associated with them will only be rendered (even if set as visible)
327    when their region is at least {minLodPixels} large and at most {maxLodPixels} large on the screen.
328    @ivar west: Bounding box coordinate
329    @type west: C{float}
330    @ivar south: Bounding box coordinate
331    @type south: C{float}
332    @ivar east: Bounding box coordinate
333    @type east: C{float}
334    @ivar north: Bounding box coordinate
335    @type north: C{float}
336    '''
337   
338    def __init__(self, west, south, east, north, minLodPixels = 64, maxLodPixels = -1):
339        self.west = west
340        self.south = south
341        self.east = east
342        self.north = north
343        self.minLodPixels = minLodPixels
344        self.maxLodPixels = maxLodPixels
345
346    def build(self):
347        llabElement = Element('LatLonAltBox')
348        SubElement(llabElement, 'west').text = str(self.west)
349        SubElement(llabElement, 'south').text = str(self.south)
350        SubElement(llabElement, 'east').text = str(self.east)
351        SubElement(llabElement, 'north').text = str(self.north)
352
353        lodElement = Element('Lod')
354        SubElement(lodElement, 'minLodPixels').text = str(self.minLodPixels)
355        SubElement(lodElement, 'maxLodPixels').text = str(self.maxLodPixels)
356
357        regionElement = Element('Region')
358        regionElement.append(llabElement)
359        regionElement.append(lodElement)
360        return regionElement
361
362class KMLGroundOverlay(KMLElement):
363    '''
364    Represents the I{<kml:GroundOverlay>} tag.
365    @ivar name: Human-readable name
366    @type name: C{str}
367    @ivar sourceUrl: URL of the image to be superimposed on the ground
368    @type sourceUrl: C{str}
369    @ivar timespanStart: Start of validity period for the overlay.
370    @type timespanStart: C{matplotlib.dates.datetime.datetime}
371    @ivar timespanEnd: End of validity period for the overlay.
372    @type timespanEnd: C{matplotlib.dates.datetime.datetime}
373    @ivar west: Bounding box coordinate
374    @type west: C{float}
375    @ivar south: Bounding box coordinate
376    @type south: C{float}
377    @ivar east: Bounding box coordinate
378    @type east: C{float}
379    @ivar north: Bounding box coordinate
380    @type north: C{float}
381    '''
382
383    def __init__(self, name, sourceUrl, timespanStart, timespanEnd, west, south, east, north, visible = True):
384        self.name = name
385        self.sourceUrl = sourceUrl
386        self.timespanStart = timespanStart
387        self.timespanEnd = timespanEnd
388        self.west = west
389        self.south = south
390        self.east = east
391        self.north = north
392        self.visible = visible
393
394    def build(self):
395
396        groundOverlayElement = Element('GroundOverlay')
397
398        SubElement(groundOverlayElement, 'name').text = self.name
399
400        if self.visible:
401            SubElement(groundOverlayElement, 'visibility').text = '1'
402        else:
403            SubElement(groundOverlayElement, 'visibility').text = '0'
404
405        timespanElement = SubElement(groundOverlayElement, 'TimeSpan')
406        SubElement(timespanElement, 'begin').text = ('%04d-%02d-%02d') % self.timespanStart.utctimetuple()[0:3]
407        SubElement(timespanElement, 'end').text = ('%04d-%02d-%02d') % self.timespanEnd.utctimetuple()[0:3]
408
409        # Include the WMS service call address
410        iconElement = SubElement(groundOverlayElement, 'icon')
411        SubElement(iconElement, 'href').text = self.sourceUrl
412        SubElement(iconElement, 'refreshMode').text = 'onExpire'
413
414        latlonboxElement=SubElement(groundOverlayElement, 'LatLonBox')
415        SubElement(latlonboxElement, 'north').text = str(self.north)
416        SubElement(latlonboxElement, 'south').text = str(self.south)
417        SubElement(latlonboxElement, 'east' ).text = str(self.east)
418        SubElement(latlonboxElement, 'west' ).text = str(self.west)
419
420        return groundOverlayElement
421
422class KMLNetworkLink(KMLElement):
423    '''
424    Represents a I{<kml:NetworkLink>} element.
425
426    Such an element is listed as a folder, but is only loaded from a fetched KML file when clicked at.
427    @ivar name: Network link name.
428    @type name: C{str}
429    @ivar url: URL from which to fetch the link contents.
430    @type url: C{str}
431    @ivar description: Human-readable description of network link contents (appears greyish below the name).
432    @type description: C{str}
433    @ivar styleID: Style identifier (see C{KMLStyle.id})
434    @type styleID: C{str}
435    @ivar visible: Determines whether the placemark is visible (that is, checked) when initially loaded
436    @type visible: C{bool}
437    '''
438
439    def __init__(self, name, url, description = None, styleID = None, visible = True):
440        self.name = name
441        self.url = url
442        self.visible = visible
443        self.styleID = styleID
444        self.description = description
445
446    def build(self):
447        networkLinkElement = Element('NetworkLink')
448
449        SubElement(networkLinkElement, 'name').text = self.name
450
451        if self.description:
452            SubElement(networkLinkElement, 'description').text = self.description
453
454        if self.styleID:
455            SubElement(networkLinkElement, 'styleUrl').text = '#' + self.styleID
456
457        if self.visible:
458            SubElement(networkLinkElement, 'visibility').text = '1'
459        else:
460            SubElement(networkLinkElement, 'visibility').text = '0'
461
462        linkElement = SubElement(networkLinkElement, 'Link')
463        SubElement(linkElement, 'href').text = self.url
464        SubElement(linkElement, 'refreshMode').text = 'onExpire'
465
466        return networkLinkElement
Note: See TracBrowser for help on using the repository browser.