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

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

Corrected bug in saving KML files, working on saving KMZ files.

Line 
1import os
2import sys
3from cElementTree import ElementTree, Element, SubElement, XML
4from zipfile import ZipFile
5from StringIO import StringIO
6
7class KMLElement:
8    '''
9    Abstract class, representing any element of a KML document -- i.e. anything contained *within*
10    the <kml:Document> element.
11    '''
12
13    def build(self):
14        '''
15        It is required that each KMLElement instance is able to build() itself into an ElementTree.Element
16        instance of KML, which it represents.
17        '''
18        raise NotImplementedError("Abstract method, to be overriden by child classes")
19
20class KMLDocument:
21    '''
22    Wraps around a whole KML document and makes it possible to save it to a file directly.
23    Represent the <kml:Document>, so that the method build() builds the kml:Document element,
24    but also contains the method save() that allows saving that to a file, wrapped within the <kml> header.
25    '''
26
27    def __init__(self, name, styles):
28
29        self.name = name
30        self.styles = styles
31        self.elements = []
32
33    def build(self):
34
35        # Create the <Document> an element to hold the document
36        documentElement = Element('Document')
37        SubElement(documentElement, 'name').text = self.name
38        SubElement(documentElement, 'open').text = '1'
39
40        # Build the associated styles
41        for style in self.styles:
42            documentElement.append( style.build() )
43
44        # Build the sub-elements
45        for element in self.elements:
46            documentElement.append( element.build() )
47       
48        return documentElement
49       
50    def save(self, outputFilename):
51        '''
52        Save the document to file <code>outputFilename</code> (full path).
53        '''
54
55        def _indentXML(elem, level=0):
56            '''Auxiliary function, indents XML'''
57            i = "\n" + level * "  "
58            if len(elem):
59                if not elem.text or not elem.text.strip():
60                    elem.text = i + "  "
61                for child in elem:
62                    _indentXML(child, level+1)
63                if not child.tail or not child.tail.strip():
64                    child.tail = i
65                if not elem.tail or not elem.tail.strip():
66                    elem.tail = i
67            else:
68                if level and (not elem.tail or not elem.tail.strip()):
69                    elem.tail = i
70
71        def _save(file):
72            '''
73            Builds and saves the document in the KML format into a file.
74            @param file: An open file-like object to write to.
75            '''
76
77            # Build the Document element
78            documentElement = self.build()
79
80            # Attach the Document element as a subelement of a root 'kml' element
81            rootElement=Element('kml', xmlns='http://earth.google.com/kml/2.2')
82            rootElement.append(documentElement)
83            _indentXML(rootElement)
84
85            # Write the KML document to a file
86            elementTree = ElementTree(rootElement)       
87            elementTree.write(file)
88
89        def _saveKml(filename):
90            '''
91            Builds and saves the document in the KML format into a file named filename.
92            @param filename: Name of the KML output file.
93            '''
94            kmlFile = open(filename, 'w')
95            _save(kmlFile)
96            kmlFile.close()
97
98        def _saveKmz(filename):
99            '''
100            Builds and saves the document into a KMZ file, i.e. using KML format with ZIP compression.
101            @param filename: Name of the KMZ output file.
102            '''
103            # Write the KMZ archive
104            buf = StringIO()
105            _save(buf)
106            kmzFile = ZipFile(filename, 'w')
107            kmzFile.writestr('doc.kml', buf.read())
108            kmzFile.close()
109            buf.close()
110
111        # Check the suffix of outputFilename, and depending on suffix, either save the document
112        # directly to a KML file, or save it packed in a KMZ file.
113        suffix = outputFilename[-3:].lower()
114        if suffix == 'kml':
115            _saveKml(outputFilename)
116        elif suffix == 'kmz':
117            #shortOutputFilename = outputFilename.split('/')[-1]          # separate out the short filename
118            #outputDir = outputFilename[:-(len(shortOutputFilename)+1)]   # separate out the directory name
119            #_save(outputDir + '/doc.kml')
120            #os.system('zip -q %s %s/doc.kml' % (outputFilename, outputDir))
121            #os.remove(outputDir + '/doc.kml')
122            _saveKmz(outputFilename)
123        else:
124            raise ValueError('Wrong file suffix, only "kml" and "kmz" allowed.')
125
126class KMLStyle(KMLElement):
127    '''
128    Represents the <kml:Style> tag.
129    '''
130
131    def __init__(self, id, iconURL = None, balloonTemplate = None, listItemType = None):
132        self.id = id
133        self.iconURL = iconURL
134        self.balloonTemplate = balloonTemplate
135        if listItemType:
136            allowedValues =  ['check', 'checkOffOnly', 'checkHideChildren', 'radioFolder']
137            if not listItemType in allowedValues:
138                raise ValueError('listItemType not among allowed values: ' + str(allowedValues))
139        self.listItemType = listItemType
140
141    def build(self):
142
143        styleElement = Element('Style')
144        styleElement.set('id', self.id)
145
146        # If specified, build the IconStyle element -- for assigning an icon to station placemarks
147        if self.iconURL:
148            iconStyleElement = SubElement(styleElement, 'IconStyle')
149            SubElement(iconStyleElement, 'scale').text = '1.2'
150            iconElement = SubElement(iconStyleElement, 'Icon')
151            SubElement(iconElement, 'href').text = self.iconURL
152
153        # If specified, build the BalloonStyle sub-element -- an HTML template for the balloons
154        if self.balloonTemplate:
155            balloonStyleElement = SubElement(styleElement, 'BalloonStyle')
156            SubElement(balloonStyleElement, 'text').text = self.balloonTemplate
157
158        # If specified, build the ListStyle sub-element -- which determines how the associated lists
159        # in the left-hand side of the Google Earth screen are going to display/behave
160        if self.listItemType:
161            listStyleElement = SubElement(styleElement, 'ListStyle')
162            SubElement(listStyleElement, 'listItemType').text = self.listItemType
163       
164        return styleElement
165
166def createDefaultPlacemarKMLStyle(id = 'default_placemark_style',
167                                  iconURL = 'http://maps.google.com/mapfiles/kml/shapes/target.png',
168                                  balloonTemplate = ''):
169    '''
170    A factory method defined for convenience. Creates a style readily usable for adding styles to KML placemarks.
171    '''
172    return KMLStyle(id, iconURL, balloonTemplate)
173
174class KMLPlacemark(KMLElement):
175    '''
176    Represents the <kml:Placemark> tag.
177    '''
178   
179    def __init__(self, id, name, lon, lat, featuresHtml, styleID = None, data=None, visible = True):
180        self.id = id
181        self.name = name
182        self.lon = lon
183        self.lat = lat
184        self.featuresHtml = featuresHtml
185        self.styleID = styleID
186        self.data = data
187        self.visible = visible
188
189    def build(self):
190        placemarkElement = Element('Placemark')
191        SubElement(placemarkElement, 'name').text = self.name
192
193        if self.visible:
194            SubElement(placemarkElement, 'visibility').text = '1'
195        else:
196            SubElement(placemarkElement, 'visibility').text = '0'
197
198        if self.styleID:
199            SubElement(placemarkElement, 'styleUrl').text = '#' + self.styleID
200
201        lookAtElement = SubElement(placemarkElement, 'LookAt')
202        SubElement(lookAtElement, 'longitude').text = str(self.lon)
203        SubElement(lookAtElement, 'latitude').text = str(self.lat)
204
205        pointElement = SubElement(placemarkElement, 'Point')
206        SubElement(pointElement, 'coordinates').text = '%f,%f,%f' % (self.lon, self.lat, 0.)
207
208        # If the "data" dictionary is provided, create the additional <ExtendedData> element,
209        # which contains specific data items, which will be automatically substituted
210        # for placemarks in the balloon template when the user views the document in Google Earth.
211        if self.data:
212            extendedDataElement = SubElement(placemarkElement, 'ExtendedData')
213            for key in self.data:
214                dataElement = SubElement(extendedDataElement, 'Data')
215                dataElement.set('name', key)
216                value = self.data[key]
217                value = value.replace('#ID#', self.id)
218                value = value.replace('#NAME#', self.name)
219                value = value.replace('#LON#', str(self.lon))
220                value = value.replace('#LAT#', str(self.lat))
221                value = value.replace('#FEATURES#', self.featuresHtml)
222                SubElement(dataElement, 'value').text = value
223               
224        return placemarkElement
225
226class KMLFolder(KMLElement):
227    '''
228    Represents the <kml:Folder> tag.
229    '''
230
231    def __init__(self, name, children, styleID = None, region = None, opened = True, visible = True, description = None):
232        self.name = name
233        self.children = children
234        self.styleID = styleID
235        self.region = region
236        self.opened = opened
237        self.visible = visible
238        self.description = description
239
240    def build(self):
241        folderElement = Element('Folder')
242        if self.styleID:
243            SubElement(folderElement, 'styleUrl').text = '#' + self.styleID
244        SubElement(folderElement, 'name').text = self.name
245        if self.description:
246            SubElement(folderElement, 'description').text = self.description
247        if self.visible:
248            SubElement(folderElement, 'visibility').text = '1'
249        else:
250            SubElement(folderElement, 'visibility').text = '0'
251        if self.opened:
252            SubElement(folderElement, 'open').text = '1'
253        else:
254            SubElement(folderElement, 'open').text = '0'
255        if self.region:
256            if not isinstance(self.region, KMLRegion):
257                raise TypeError('Region not a KMLRegion')
258            folderElement.append( self.region.build() )
259        for child in self.children:
260            if not isinstance(child, KMLElement):
261                raise TypeError('Child does not have a KMLElement base class')
262            folderElement.append( child.build() )
263        return folderElement
264
265class KMLRegion(KMLElement):
266    '''
267    Represents the <kml:Region> tag.
268    '''
269   
270    def __init__(self, west, south, east, north, minLodPixels = 64, maxLodPixels = -1):
271        self.west = west
272        self.south = south
273        self.east = east
274        self.north = north
275        self.minLodPixels = minLodPixels
276        self.maxLodPixels = maxLodPixels
277
278    def build(self):
279        llabElement = Element('LatLonAltBox')
280        SubElement(llabElement, 'west').text = str(self.west)
281        SubElement(llabElement, 'south').text = str(self.south)
282        SubElement(llabElement, 'east').text = str(self.east)
283        SubElement(llabElement, 'north').text = str(self.north)
284
285        lodElement = Element('Lod')
286        SubElement(lodElement, 'minLodPixels').text = str(self.minLodPixels)
287        SubElement(lodElement, 'maxLodPixels').text = str(self.maxLodPixels)
288
289        regionElement = Element('Region')
290        regionElement.append(llabElement)
291        regionElement.append(lodElement)
292        return regionElement
293
294class KMLGroundOverlay(KMLElement):
295    '''Represents the <kml:GroundOverlay> tag.'''
296
297    def __init__(self, name, sourceUrl, timespanStart, timespanEnd, west, south, east, north, visible = True):
298        self.name = name
299        self.sourceUrl = sourceUrl
300        self.timespanStart = timespanStart
301        self.timespanEnd = timespanEnd
302        self.west = west
303        self.south = south
304        self.east = east
305        self.north = north
306        self.visible = visible
307
308    def build(self):
309
310        groundOverlayElement = Element('GroundOverlay')
311
312        SubElement(groundOverlayElement, 'name').text = self.name
313
314        if self.visible:
315            SubElement(groundOverlayElement, 'visibility').text = '1'
316        else:
317            SubElement(groundOverlayElement, 'visibility').text = '0'
318
319        timespanElement = SubElement(groundOverlayElement, 'TimeSpan')
320        SubElement(timespanElement, 'begin').text = ('%04d-%02d-%02d') % self.timespanStart.utctimetuple()[0:3]
321        SubElement(timespanElement, 'end').text = ('%04d-%02d-%02d') % self.timespanEnd.utctimetuple()[0:3]
322
323        # Include the WMS service call address
324        iconElement = SubElement(groundOverlayElement, 'icon')
325        SubElement(iconElement, 'href').text = self.sourceUrl
326        SubElement(iconElement, 'refreshMode').text = 'onExpire'
327
328        latlonboxElement=SubElement(groundOverlayElement, 'LatLonBox')
329        SubElement(latlonboxElement, 'north').text = str(self.north)
330        SubElement(latlonboxElement, 'south').text = str(self.south)
331        SubElement(latlonboxElement, 'east' ).text = str(self.east)
332        SubElement(latlonboxElement, 'west' ).text = str(self.west)
333
334        return groundOverlayElement
335
336class KMLNetworkLink(KMLElement):
337
338    def __init__(self, name, url, visible = True):
339        self.name = name
340        self.url = url
341        self.visible = visible
342
343    def build(self):
344        networkLinkElement = Element('NetworkLink')
345
346        SubElement(networkLinkElement, 'name').text = self.name
347
348        if self.visible:
349            SubElement(networkLinkElement, 'visibility').text = '1'
350        else:
351            SubElement(networkLinkElement, 'visibility').text = '0'
352
353        linkElement = SubElement(networkLinkElement, 'Link')
354        SubElement(linkElement, 'href').text = self.url
355
356        return networkLinkElement
Note: See TracBrowser for help on using the repository browser.