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

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

Extended StationConvertor? to handle colouring of stations. Added config file for the ECN dataset.

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 ET 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 iconColor: Can contain a color to be blended with the icon image, in hexadecimal RGBA format
153    (for example, "00ff00ff" would mean green with 100% alpha).
154    @type iconColor: C{str}
155    @ivar balloonTemplate: Can contain a HTML template with variables that are substituted during build
156    using from C{KMLPlacemark.data} attributes;
157    to be associated with C{KMLPlacemark} objects.
158    @type balloonTemplate: C{str}
159    @ivar listItemType: Can contain a string designating listing mode for a KML folder;
160    to be associated with C{KMLFolder} objects;
161    permitted values are: "C{check}", "C{checkOffOnly}", "C{checkHideChildren}", "C{radioFolder}".
162    @type listItemType: C{str}
163    '''
164
165    def __init__(self, id, iconURL = None, iconColor = 'ffffffff', balloonTemplate = None, listItemType = None):
166        self.id = id
167        self.iconURL = iconURL
168        self.iconColor = iconColor
169        self.balloonTemplate = balloonTemplate
170        if listItemType:
171            allowedValues = ['check', 'checkOffOnly', 'checkHideChildren', 'radioFolder']
172            if not listItemType in allowedValues:
173                raise ValueError('listItemType not among allowed values: ' + str(allowedValues))
174        self.listItemType = listItemType
175
176    def build(self):
177
178        styleElement = Element('Style')
179        styleElement.set('id', self.id)
180
181        # If specified, build the IconStyle element -- for assigning an icon to station placemarks
182        if self.iconURL:
183            iconStyleElement = SubElement(styleElement, 'IconStyle')
184            SubElement(iconStyleElement, 'scale').text = '1.2'           
185            SubElement(iconStyleElement, 'color').text = self.iconColor
186            iconElement = SubElement(iconStyleElement, 'Icon')
187            SubElement(iconElement, 'href').text = self.iconURL
188
189        # If specified, build the BalloonStyle sub-element -- an HTML template for the balloons
190        if self.balloonTemplate:
191            balloonStyleElement = SubElement(styleElement, 'BalloonStyle')
192            SubElement(balloonStyleElement, 'text').text = self.balloonTemplate
193
194        # If specified, build the ListStyle sub-element -- which determines how the associated lists
195        # in the left-hand side of the Google Earth screen are going to display/behave
196        if self.listItemType:
197            listStyleElement = SubElement(styleElement, 'ListStyle')
198            SubElement(listStyleElement, 'listItemType').text = self.listItemType
199       
200        return styleElement
201
202def createDefaultPlacemarKMLStyle(id = 'default_placemark_style',
203                                  iconURL = 'http://maps.google.com/mapfiles/kml/shapes/target.png',
204                                  balloonTemplate = ''):
205    '''
206    A factory method defined for convenience. Creates a "default placemark style".
207    @rtype: C{KMLStyle}
208    '''
209    return KMLStyle(id, iconURL = iconURL, balloonTemplate = balloonTemplate)
210
211class KMLPlacemark(KMLElement):
212    '''
213    Represents the I{<kml:Placemark>} tag.
214    @ivar id: Placemark identifier
215    @type id: C{str}
216    @ivar name: Human-readable name
217    @type name: C{str}
218    @ivar lon: Longitude
219    @type lon: C{float}
220    @ivar lat: Latitude
221    @type lat: C{float}
222    @ivar styleID: Style identifier (see C{KMLStyle.id})
223    @type styleID: C{str}
224    @ivar data: A dictionary containing instance-specific data items which are to be substituted
225    for in the balloon template by the values defined by C{self.data}.
226    @type data: C{dict}
227    @ivar visible: Determines whether the placemark is visible (ie. checked) initially when loaded
228    @type visible: C{bool}
229    '''
230   
231    def __init__(self, id, name, lon, lat, styleID = None, data=None, visible = True):
232        self.id = id
233        self.name = name
234        self.lon = lon
235        self.lat = lat
236        self.styleID = styleID
237        self.data = data
238        self.visible = visible
239
240    def build(self):
241        placemarkElement = Element('Placemark')
242        SubElement(placemarkElement, 'name').text = self.name
243
244        if self.visible:
245            SubElement(placemarkElement, 'visibility').text = '1'
246        else:
247            SubElement(placemarkElement, 'visibility').text = '0'
248
249        if self.styleID:
250            SubElement(placemarkElement, 'styleUrl').text = '#' + self.styleID
251
252        lookAtElement = SubElement(placemarkElement, 'LookAt')
253        SubElement(lookAtElement, 'longitude').text = str(self.lon)
254        SubElement(lookAtElement, 'latitude').text = str(self.lat)
255
256        pointElement = SubElement(placemarkElement, 'Point')
257        SubElement(pointElement, 'coordinates').text = '%f,%f,%f' % (self.lon, self.lat, 0.)
258
259        # If the "data" dictionary is provided, create the additional <ExtendedData> element,
260        # which contains specific data items, which will be automatically substituted
261        # for placemarks in the balloon template when the user views the document in Google Earth.
262        if self.data:
263            extendedDataElement = SubElement(placemarkElement, 'ExtendedData')
264            for key in self.data:
265                dataElement = SubElement(extendedDataElement, 'Data')
266                dataElement.set('name', key)
267                value = self.data[key]
268                value = value.replace('#ID#', self.id)
269                value = value.replace('#NAME#', self.name)
270                SubElement(dataElement, 'value').text = value
271               
272        return placemarkElement
273
274class KMLFolder(KMLElement):
275    '''
276    Represents the I{<kml:Folder>} tag.
277    @ivar name: Human-readable folder name
278    @type name: C{str}
279    @ivar description: Human-readable detailed description of folder contents (appears greyish below the name).
280    @type description: C{str}
281    @ivar children: A list of embedded elements.
282    @type children: C{KMLElement list}
283    @ivar styleID: Style identifier (see C{KMLStyle.id})
284    @type styleID: C{str}
285    @ivar region: A region associated with this folder.
286    @type region: C{KMLRegion}
287    @ivar opened: Determines whether the placemark is opened (that is, listed) when initially loaded
288    @type opened: C{bool}
289    @ivar visible: Determines whether the placemark is visible (that is, checked) when initially loaded
290    @type visible: C{bool}
291    '''
292
293    def __init__(self, name, children, styleID = None, region = None, opened = True, visible = True, description = None):
294        self.name = name
295        self.children = children
296        self.styleID = styleID
297        self.region = region
298        self.opened = opened
299        self.visible = visible
300        self.description = description
301
302    def build(self):
303        folderElement = Element('Folder')
304        if self.styleID:
305            SubElement(folderElement, 'styleUrl').text = '#' + self.styleID
306        SubElement(folderElement, 'name').text = self.name
307        if self.description:
308            SubElement(folderElement, 'description').text = self.description
309        if self.visible:
310            SubElement(folderElement, 'visibility').text = '1'
311        else:
312            SubElement(folderElement, 'visibility').text = '0'
313        if self.opened:
314            SubElement(folderElement, 'open').text = '1'
315        else:
316            SubElement(folderElement, 'open').text = '0'
317        if self.region:
318            if not isinstance(self.region, KMLRegion):
319                raise TypeError('Region not a KMLRegion')
320            folderElement.append( self.region.build() )
321        for child in self.children:
322            if not isinstance(child, KMLElement):
323                raise TypeError('Child does not have a KMLElement base class')
324            folderElement.append( child.build() )
325        return folderElement
326
327class KMLRegion(KMLElement):
328    '''
329    Represents the I{<kml:Region>} tag.
330
331    Elements which have a region associated with them will only be rendered (even if set as visible)
332    when their region is at least {minLodPixels} large and at most {maxLodPixels} large on the screen.
333    @ivar west: Bounding box coordinate
334    @type west: C{float}
335    @ivar south: Bounding box coordinate
336    @type south: C{float}
337    @ivar east: Bounding box coordinate
338    @type east: C{float}
339    @ivar north: Bounding box coordinate
340    @type north: C{float}
341    '''
342   
343    def __init__(self, west, south, east, north, minLodPixels = 64, maxLodPixels = -1):
344        self.west = west
345        self.south = south
346        self.east = east
347        self.north = north
348        self.minLodPixels = minLodPixels
349        self.maxLodPixels = maxLodPixels
350
351    def build(self):
352        llabElement = Element('LatLonAltBox')
353        SubElement(llabElement, 'west').text = str(self.west)
354        SubElement(llabElement, 'south').text = str(self.south)
355        SubElement(llabElement, 'east').text = str(self.east)
356        SubElement(llabElement, 'north').text = str(self.north)
357
358        lodElement = Element('Lod')
359        SubElement(lodElement, 'minLodPixels').text = str(self.minLodPixels)
360        SubElement(lodElement, 'maxLodPixels').text = str(self.maxLodPixels)
361
362        regionElement = Element('Region')
363        regionElement.append(llabElement)
364        regionElement.append(lodElement)
365        return regionElement
366
367class KMLGroundOverlay(KMLElement):
368    '''
369    Represents the I{<kml:GroundOverlay>} tag.
370    @ivar name: Human-readable name
371    @type name: C{str}
372    @ivar sourceUrl: URL of the image to be superimposed on the ground
373    @type sourceUrl: C{str}
374    @ivar timespanStart: Start of validity period for the overlay.
375    @type timespanStart: C{matplotlib.dates.datetime.datetime}
376    @ivar timespanEnd: End of validity period for the overlay.
377    @type timespanEnd: C{matplotlib.dates.datetime.datetime}
378    @ivar west: Bounding box coordinate
379    @type west: C{float}
380    @ivar south: Bounding box coordinate
381    @type south: C{float}
382    @ivar east: Bounding box coordinate
383    @type east: C{float}
384    @ivar north: Bounding box coordinate
385    @type north: C{float}
386    '''
387
388    def __init__(self, name, sourceUrl, timespanStart, timespanEnd, west, south, east, north, visible = True):
389        self.name = name
390        self.sourceUrl = sourceUrl
391        self.timespanStart = timespanStart
392        self.timespanEnd = timespanEnd
393        self.west = west
394        self.south = south
395        self.east = east
396        self.north = north
397        self.visible = visible
398
399    def build(self):
400
401        groundOverlayElement = Element('GroundOverlay')
402
403        SubElement(groundOverlayElement, 'name').text = self.name
404
405        if self.visible:
406            SubElement(groundOverlayElement, 'visibility').text = '1'
407        else:
408            SubElement(groundOverlayElement, 'visibility').text = '0'
409
410        timespanElement = SubElement(groundOverlayElement, 'TimeSpan')
411        SubElement(timespanElement, 'begin').text = ('%04d-%02d-%02d') % self.timespanStart.utctimetuple()[0:3]
412        SubElement(timespanElement, 'end').text = ('%04d-%02d-%02d') % self.timespanEnd.utctimetuple()[0:3]
413
414        # Include the WMS service call address
415        iconElement = SubElement(groundOverlayElement, 'icon')
416        SubElement(iconElement, 'href').text = self.sourceUrl
417        SubElement(iconElement, 'refreshMode').text = 'onExpire'
418
419        latlonboxElement=SubElement(groundOverlayElement, 'LatLonBox')
420        SubElement(latlonboxElement, 'north').text = str(self.north)
421        SubElement(latlonboxElement, 'south').text = str(self.south)
422        SubElement(latlonboxElement, 'east' ).text = str(self.east)
423        SubElement(latlonboxElement, 'west' ).text = str(self.west)
424
425        return groundOverlayElement
426
427class KMLNetworkLink(KMLElement):
428    '''
429    Represents a I{<kml:NetworkLink>} element.
430
431    Such an element is listed as a folder, but is only loaded from a fetched KML file when clicked at.
432    @ivar name: Network link name.
433    @type name: C{str}
434    @ivar url: URL from which to fetch the link contents.
435    @type url: C{str}
436    @ivar description: Human-readable description of network link contents (appears greyish below the name).
437    @type description: C{str}
438    @ivar styleID: Style identifier (see C{KMLStyle.id})
439    @type styleID: C{str}
440    @ivar visible: Determines whether the placemark is visible (that is, checked) when initially loaded
441    @type visible: C{bool}
442    '''
443
444    def __init__(self, name, url, description = None, styleID = None, visible = True):
445        self.name = name
446        self.url = url
447        self.visible = visible
448        self.styleID = styleID
449        self.description = description
450
451    def build(self):
452        networkLinkElement = Element('NetworkLink')
453
454        SubElement(networkLinkElement, 'name').text = self.name
455
456        if self.description:
457            SubElement(networkLinkElement, 'description').text = self.description
458
459        if self.styleID:
460            SubElement(networkLinkElement, 'styleUrl').text = '#' + self.styleID
461
462        if self.visible:
463            SubElement(networkLinkElement, 'visibility').text = '1'
464        else:
465            SubElement(networkLinkElement, 'visibility').text = '0'
466
467        linkElement = SubElement(networkLinkElement, 'Link')
468        SubElement(linkElement, 'href').text = self.url
469        SubElement(linkElement, 'refreshMode').text = 'onExpire'
470
471        return networkLinkElement
Note: See TracBrowser for help on using the repository browser.