pyKML Tutorial

The following tutorial gives a brief overview of many of the features of pyKML. It is designed to run from within a Python or iPython shell, and assumes that pyKML has been installed and is part of your Python search path. The tutorial is designed to be followed from start to finish.

For complete stand alone programs that demonstrate how to use pyKML, check out the pyKML Examples.

Constructing KML from scratch

The pyKML library can be used to construct KML documents, using the pykml.factory module:

# create a factory object that can create elements in the KML namespace
In [2]: from pykml.factory import KML_ElementMaker as KML

# create an the object equivalent of a KML <name> element
In [3]: name_object = KML.name("Hello World!")

If you are creating KML documents that utilize elements that are not part of the default KML namespace, you will want to create an additional factory objects for each namespace. For example, the following creates factory objects that can be used to create elements that are part of the ATOM and Google Extensions namespace:

In [4]: from pykml.factory import ATOM_ElementMaker as ATOM

In [5]: from pykml.factory import GX_ElementMaker as GX

Documents with nested KML tags can be created by nesting the creation of Python objects:

In [6]: pm1 = KML.Placemark(
   ...:          KML.name("Hello World!"),
   ...:          KML.Point(
   ...:            KML.coordinates("-64.5253,18.4607")
   ...:          )
   ...:        )

Once a pyKML object element has been created, a string representation can be generated by using the .tostring() method:

In [12]: from lxml import etree

In [13]: etree.tostring(pm1)
Out[13]: '<Placemark xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://www.opengis.net/kml/2.2"><name>Hello World!</name><Point><coordinates>-64.5253,18.4607</coordinates></Point></Placemark>'

# use the pretty_print keyword if you want something more readable
In [14]: print etree.tostring(pm1, pretty_print=True)
<Placemark xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://www.opengis.net/kml/2.2">
  <name>Hello World!</name>
  <Point>
    <coordinates>-64.5253,18.4607</coordinates>
  </Point>
</Placemark>

pyKML creates Python objects, which can be passed around and later aggregated. The following creates a second placemark object, and then groups the two placemarks together in a folder.

# create another placemark
In [15]: pm2 = KML.Placemark(
   ....:          KML.name("A second placemark!"),
   ....:          KML.Point(
   ....:            KML.coordinates("-64.5358,18.4486")
   ....:          )
   ....:        )

# group the two placemarks in a folder
In [21]: fld = KML.Folder(pm1,pm2)

In [22]: print etree.tostring(fld, pretty_print=True)
<Folder xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://www.opengis.net/kml/2.2">
  <Placemark>
    <name>Hello World!</name>
    <Point>
      <coordinates>-64.5253,18.4607</coordinates>
    </Point>
  </Placemark>
  <Placemark>
    <name>A second placemark!</name>
    <Point>
      <coordinates>-64.5358,18.4486</coordinates>
    </Point>
  </Placemark>
</Folder>

Objects representing KML elements can also be appended into objects that have already been created. For example, the following appends yet another placemark to the folder.

# create yet another placemark
In [23]: pm3=KML.Placemark(KML.name("A third placemark!"))

# append the placemark to the series already in the folder
In [24]: fld.append(pm3)

In [25]: print etree.tostring(fld, pretty_print=True)
<Folder xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://www.opengis.net/kml/2.2">
  <Placemark>
    <name>Hello World!</name>
    <Point>
      <coordinates>-64.5253,18.4607</coordinates>
    </Point>
  </Placemark>
  <Placemark>
    <name>A second placemark!</name>
    <Point>
      <coordinates>-64.5358,18.4486</coordinates>
    </Point>
  </Placemark>
  <Placemark>
    <name>A third placemark!</name>
  </Placemark>
</Folder>

Similarly, you can remove elements from an existing object. The following removes the second of three placemarks from the folder:

# remove a particular placemark
In [26]: fld.remove(pm2)

In [27]: print etree.tostring(fld, pretty_print=True)
<Folder xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://www.opengis.net/kml/2.2">
  <Placemark>
    <name>Hello World!</name>
    <Point>
      <coordinates>-64.5253,18.4607</coordinates>
    </Point>
  </Placemark>
  <Placemark>
    <name>A third placemark!</name>
  </Placemark>
</Folder>

Once you have a KML document, you can access elements using object attributes:

In [28]: print fld.Placemark.name.text
Hello World!

This type of attribute-based access is provided by the lxml packages’s objectify API. pyKML users are encouraged to familiarize themselves with the objectify API documentation on the lxml website, because pyKML inherits this functionality.

Parsing existing KML documents

Sometimes instead of building a KML document from scratch, you may want to modify an existing KML document. For this case, pyKML’s parsing capabilities are useful. pyKML can parse information from a variety of sources, including strings, local files, and remote URLs.

The most straightforward is parsing from a string...

In [29]: from pykml import parser

In [30]: kml_str = '<kml xmlns="http://www.opengis.net/kml/2.2">' \
   ....:              '<Document>' \
   ....:                '<Folder>' \
   ....:                  '<name>sample folder</name>' \
   ....:                '</Folder>' \
   ....:              '</Document>' \
   ....:            '</kml>'

In [37]: root = parser.fromstring(kml_str)

In [38]: print root.Document.Folder.name.text
sample folder

You can also parse a local file...

In [39]: from os import path

In [40]: kml_file = path.join( \
   ....:      '../src/pykml/test', \
   ....:      'testfiles/google_kml_developers_guide', \
   ....:      'complete_tour_example.kml')

In [44]: with open(kml_file) as f:

In [45]:     doc = parser.parse(f)

In [46]: 

... or a remote URL...

In [47]: import urllib2

In [48]: url = 'http://code.google.com/apis/kml/documentation/KML_Samples.kml'

In [49]: fileobject = urllib2.urlopen(url)

In [50]: root = parser.parse(fileobject).getroot()

In [51]: print root.Document.name
KML Samples

Validation of KML documents

KML documents that you create can be validated against XML Schema documents, which define the rules of which elements are acceptible and what ordering can be used. Both the OGC KML schema and the Google Extension schemas are included with pyKML.

To validate your KML document, first create instances of the schemas:

In [52]: from pykml.parser import Schema

In [53]: schema_ogc = Schema("ogckml22.xsd")

In [54]: schema_gx = Schema("kml22gx.xsd")

Then use the schemas to validate your KML objects, using the .validate() or .assertValid() methods. The following code creates a small invalide KML document which includes an element from the Google Extension namespace (<gx_Tour>) so the document does not validate against the basic OGC KML schema, but does validate agains the Google Extensions schema.

# create a small KML document
In [55]: doc = KML.kml(GX.Tour())

# validate it against the OGC KML schema
In [56]: schema_ogc.validate(doc)
Out[56]: False

# validate it against the Google Extension schema
In [57]: schema_gx.validate(doc)
Out[57]: True

The .validate() method only returns True or False. For invalid documents, it is often useful to obtain details of why the document is invalid using the .assertValid() method:

# validate against the OGC KML schema, and generate an exception
In [58]: schema_ogc.assertValid(doc)
---------------------------------------------------------------------------
DocumentInvalid                           Traceback (most recent call last)

/home/tylere/Dropbox/project/pykml/docs/<ipython console> in <module>()

/home/tylere/Dropbox/project/pykml/src/pykml/parser.pyc in assertValid(self, doc)
     28         when compared to the XML Schema.
     29         """
---> 30         return self.schema.assertValid(doc)
     31 
     32 def fromstring(text, schema=None):

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree._Validator.assertValid (src/lxml/lxml.etree.c:125415)()

DocumentInvalid: Element '{http://www.google.com/kml/ext/2.2}Tour': This element is not expected. Expected is one of ( {http://www.opengis.net/kml/2.2}NetworkLinkControl, {http://www.opengis.net/kml/2.2}AbstractFeatureGroup, {http://www.opengis.net/kml/2.2}Document, {http://www.opengis.net/kml/2.2}Folder, {http://www.opengis.net/kml/2.2}Placemark, {http://www.opengis.net/kml/2.2}NetworkLink, {http://www.opengis.net/kml/2.2}GroundOverlay, {http://www.opengis.net/kml/2.2}ScreenOverlay, {http://www.opengis.net/kml/2.2}PhotoOverlay ).

You can also validate while parsing by including a schema object as a parameter.

# the following triggers an error because <eggplant> is not a valid OGC KML element
In [59]: bad_kml_str = '<kml xmlns="http://www.opengis.net/kml/2.2">' \
   ....:              '<Document>' \
   ....:                '<Folder>' \
   ....:                  '<eggplant/>' \
   ....:                '</Folder>' \
   ....:              '</Document>' \
   ....:            '</kml>'

In [66]: root = parser.fromstring(bad_kml_str, schema_ogc)
---------------------------------------------------------------------------
XMLSyntaxError                            Traceback (most recent call last)

/home/tylere/Dropbox/project/pykml/docs/<ipython console> in <module>()

/home/tylere/Dropbox/project/pykml/src/pykml/parser.pyc in fromstring(text, schema)
     37     if schema:
     38         parser = objectify.makeparser(schema = schema.schema)
---> 39         return objectify.fromstring(text, parser=parser)
     40     else:
     41         return objectify.fromstring(text)

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/objectify.so in lxml.objectify.fromstring (src/lxml/lxml.objectify.c:18487)()

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree.fromstring (src/lxml/lxml.etree.c:52665)()

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree._parseMemoryDocument (src/lxml/lxml.etree.c:79932)()

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree._parseDoc (src/lxml/lxml.etree.c:78774)()

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree._BaseParser._parseDoc (src/lxml/lxml.etree.c:75389)()

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree._ParserContext._handleParseResultDoc (src/lxml/lxml.etree.c:71739)()

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree._handleParseResult (src/lxml/lxml.etree.c:72614)()

/home/tylere/.virtualenvs/pykml-doc/lib/python2.7/site-packages/lxml/etree.so in lxml.etree._raiseParseError (src/lxml/lxml.etree.c:71955)()

XMLSyntaxError: Element '{http://www.opengis.net/kml/2.2}eggplant': This element is not expected. Expected is one of ( {http://www.opengis.net/kml/2.2}name, {http://www.opengis.net/kml/2.2}visibility, {http://www.opengis.net/kml/2.2}open, {http://www.w3.org/2005/Atom}author, {http://www.w3.org/2005/Atom}link, {http://www.opengis.net/kml/2.2}address, {urn:oasis:names:tc:ciq:xsdschema:xAL:2.0}AddressDetails, {http://www.opengis.net/kml/2.2}phoneNumber, {http://www.opengis.net/kml/2.2}Snippet, {http://www.opengis.net/kml/2.2}snippet ).

Setting the Number of Decimal Places

Many KML files, especially those authored by Google Earth, contain coordinate information with more decimal places that often is necessary. The set_max_decimal_places() function addresses this, by allowing a user to reduce the number of decimal places used. The example below demonstrates this for a previously created placemark.

In [67]: from pykml.helpers import set_max_decimal_places

In [68]: print etree.tostring(pm1, pretty_print=True)
<Placemark xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
  <name>Hello World!</name>
  <Point>
    <coordinates>-64.5253,18.4607</coordinates>
  </Point>
</Placemark>


# set the coordinate precision to something smaller
In [69]: set_max_decimal_places(
   ....:             pm1,
   ....:             max_decimals={
   ....:                 'longitude': 2,
   ....:                 'latitude': 1,
   ....:             }
   ....:         )

# note that the coordinate values have changed
In [76]: print etree.tostring(pm1, pretty_print=True)
<Placemark xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
  <name>Hello World!</name>
  <Point>
    <coordinates>-64.53,18.5</coordinates>
  </Point>
</Placemark>

Building pyKML Python Scripts

While pyKML allows you use leverage programming to create customized KML files, writing the initial pyKML code can be tedious. To help with this, pyKML provides the verbosely named .write_python_script_for_kml_document() function which will produce a Python script that can serve as a starting point for further customization.

In [77]: from pykml.factory import write_python_script_for_kml_document

In [78]: url = 'http://code.google.com/apis/kml/documentation/kmlfiles/altitudemode_reference.kml'

In [79]: fileobject = urllib2.urlopen(url)

In [80]: doc = parser.parse(fileobject).getroot()

In [81]: script = write_python_script_for_kml_document(doc)

In [82]: print script
from lxml import etree
from pykml.factory import KML_ElementMaker as KML
from pykml.factory import ATOM_ElementMaker as ATOM
from pykml.factory import GX_ElementMaker as GX

doc = KML.kml(
  etree.Comment(' required when using gx-prefixed elements '),
  KML.Placemark(
    KML.name('gx:altitudeMode Example'),
    KML.LookAt(
      KML.longitude('146.806'),
      KML.latitude('12.219'),
      KML.heading('-60'),
      KML.tilt('70'),
      KML.range('6300'),
      GX.altitudeMode('relativeToSeaFloor'),
    ),
    KML.LineString(
      KML.extrude('1'),
      GX.altitudeMode('relativeToSeaFloor'),
      KML.coordinates(
      '146.825,12.233,400'
      '146.820,12.222,400'
      '146.812,12.212,400'
      '146.796,12.209,400'
      '146.788,12.205,400'
      ),
    ),
  ),
)
print etree.tostring(etree.ElementTree(doc),pretty_print=True)

That concludes the tutorial. For further examples of how pyKML can be used, head on over to the pyKML Examples section of the documentation.