Skip to content

Commit

Permalink
add Schema for typed ExtendedData
Browse files Browse the repository at this point in the history
  • Loading branch information
cleder committed Nov 26, 2013
1 parent 09cfe42 commit b810f3b
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 9 deletions.
1 change: 1 addition & 0 deletions fastkml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .kml import KML, Document, Folder, Placemark
from .kml import TimeSpan, TimeStamp
from .kml import UntypedExtendedDataElement, UntypedExtendedData
from .kml import Schema

from .styles import StyleUrl, Style, StyleMap
from .styles import IconStyle, LineStyle, PolyStyle
Expand Down
144 changes: 140 additions & 4 deletions fastkml/kml.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,12 @@ def snippet(self, snip=None):
self._snippet = {}
if isinstance(snip, dict):
self._snippet['text'] = snip.get('text')
self._snippet['maxLines'] = int(snip.get('maxLines',0))
elif isinstance(text,basestring):
max_lines = snip.get('maxLines')
if max_lines is not None:
self._snippet['maxLines'] = int(snip['maxLines'])
elif isinstance(snip, basestring):
self._snippet['text'] = snip
elif text is None:
elif snip is None:
self._snippet = None
else:
raise ValueError("Snippet must be dict of {'text':t, 'maxLines':i} or string")
Expand Down Expand Up @@ -577,9 +579,25 @@ def append(self, kmlobj):
class Document(_Container):
"""
A Document is a container for features and styles. This element is
required if your KML file uses shared styles
required if your KML file uses shared styles or schemata for typed
extended data
"""
__name__ = "Document"
_schemata = None

def schemata(self):
if self._schemata:
for schema in self._schemata:
yield schema

def append_schema(self, schema):
if self._schemata is None:
self._schemata = []
if isinstance(schema, Schema):
self._schemata.append(schema)
else:
s = Schema(schema)
self._schemata.append(s)

def from_element(self, element):
super(Document, self).from_element(element)
Expand All @@ -593,6 +611,18 @@ def from_element(self, element):
feature = Placemark(self.ns)
feature.from_element(placemark)
self.append(feature)
schemata = element.findall('%sSchema' % self.ns)
for schema in schemata:
s = Schema(self.ns, id = 'default')
s.from_element(schema)
self.append_schema(s)

def etree_element(self):
element = super(Document, self).etree_element()
if self._schemata is not None:
for schema in self._schemata:
element.append(schema.etree_element())
return element

def get_style_by_url(self, styleUrl):
id = urlparse.urlparse(styleUrl).fragment
Expand Down Expand Up @@ -833,6 +863,112 @@ def etree_element(self):
return element


class Schema(_BaseObject):
"""
Specifies a custom KML schema that is used to add custom data to
KML Features.
The "id" attribute is required and must be unique within the KML file.
<Schema> is always a child of <Document>.
"""
__name__ = "Schema"

_simple_fields = None
# The declaration of the custom fields, each of which must specify both the
# type and the name of this field. If either the type or the name is
# omitted, the field is ignored.

def __init__(self, ns=None, id=None, fields=None):
if id is None:
raise ValueError('Id is required for schema')
super(Schema, self).__init__(ns, id)
self.simple_fields = fields

@property
def simple_fields(self):
sfs = []
for simple_field in self._simple_fields:
if simple_field.get('type') and simple_field.get('name'):
sfs.append( {'type': simple_field['type'],
'name': simple_field['name'],
'displayName': simple_field.get('displayName')})
return tuple(sfs)

@simple_fields.setter
def simple_fields(self, fields):
self._simple_fields = []
if isinstance(fields, dict):
self.append(**fields)
elif isinstance(fields, (list, tuple)):
for field in fields:
if isinstance(field, (list, tuple)):
self.append(*field)
elif isinstance(field, dict):
self.append(**field)
elif fields is None:
self._simple_fields = []
else:
raise ValueError('Fields must be of type list, tuple or dict')

def append(self, type, name, displayName=None):
"""
append a field.
The declaration of the custom field, must specify both the type
and the name of this field.
If either the type or the name is omitted, the field is ignored.
The type can be one of the following:
string
int
uint
short
ushort
float
double
bool
<displayName>
The name, if any, to be used when the field name is displayed to
the Google Earth user. Use the [CDATA] element to escape standard
HTML markup.
"""
allowed_types= ['string', 'int', 'uint', 'short', 'ushort',
'float', 'double', 'bool']
if type not in allowed_types:
raise TypeError("type must be one of 'string', 'int', 'uint', 'short', 'ushort', 'float', 'double', 'bool'")
else:
#TODO explicit type conversion to check for the right type
pass
self._simple_fields.append({'type': type, 'name': name,
'displayName': displayName})

def from_element(self, element):
super(Schema, self).from_element(element)
simple_fields = element.findall('%sSimpleField' % self.ns)
self.simple_fields = None
for simple_field in simple_fields:
sfname = simple_field.get('name')
sftype = simple_field.get('type')
display_name = simple_field.find('%sdisplayName' % self.ns)
if display_name is not None:
sfdisplay_name = display_name.text
else:
sfdisplay_name = None
self.append(sftype, sfname, sfdisplay_name)

def etree_element(self):
element = super(Schema, self).etree_element()
for simple_field in self.simple_fields:
sf = etree.SubElement(element, "%sSimpleField" %self.ns)
sf.set('type', simple_field['type'])
sf.set('name', simple_field['name'])
if simple_field.get('displayName'):
dn = etree.SubElement(sf, "%sdisplayName" %self.ns)
dn.text = simple_field['displayName']

return element



class UntypedExtendedData(_BaseObject):
""" Represents a list of untyped name/value pairs. See docs:
Expand Down
83 changes: 78 additions & 5 deletions fastkml/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,32 @@ def test_placemark(self):
k2.from_string(k.to_string(prettyprint=True))
self.assertEqual(k.to_string(), k2.to_string())

def test_schema(self):
ns = '{http://www.opengis.net/kml/2.2}'
self.assertRaises(ValueError, kml.Schema, ns)
s = kml.Schema(ns, 'some_id')
self.assertEqual(len(list(s.simple_fields)),0)
s.append('int', 'Integer', 'An Integer')
self.assertEqual(list(s.simple_fields)[0]['type'], 'int')
self.assertEqual(list(s.simple_fields)[0]['name'], 'Integer')
self.assertEqual(list(s.simple_fields)[0]['displayName'], 'An Integer')
s.simple_fields = None
self.assertEqual(len(list(s.simple_fields)),0)
self.assertRaises(TypeError, s.append, ('none', 'Integer', 'An Integer'))
self.assertRaises(TypeError, s.simple_fields, [('none', 'Integer', 'An Integer'),])
self.assertRaises(TypeError, s.simple_fields, ('int', 'Integer', 'An Integer'))
fields = {'type': 'int', 'name': 'Integer', 'displayName': 'An Integer'}
s.simple_fields = fields
self.assertEqual(list(s.simple_fields)[0]['type'], 'int')
self.assertEqual(list(s.simple_fields)[0]['name'], 'Integer')
self.assertEqual(list(s.simple_fields)[0]['displayName'], 'An Integer')
s.simple_fields = [['float', 'Float'], fields]
self.assertEqual(list(s.simple_fields)[0]['type'], 'float')
self.assertEqual(list(s.simple_fields)[0]['name'], 'Float')
self.assertEqual(list(s.simple_fields)[0]['displayName'], None)
self.assertEqual(list(s.simple_fields)[1]['type'], 'int')
self.assertEqual(list(s.simple_fields)[1]['name'], 'Integer')
self.assertEqual(list(s.simple_fields)[1]['displayName'], 'An Integer')

def test_untyped_extended_data(self):
ns = '{http://www.opengis.net/kml/2.2}'
Expand Down Expand Up @@ -548,22 +574,69 @@ def test_multipolygon(self):
def test_atom(self):
pass

def test_schema(self):
doc = """<Schema name="TrailHeadType" id="TrailHeadTypeId">
<SimpleField type="string" name="TrailHeadName">
<displayName><![CDATA[<b>Trail Head Name</b>]]></displayName>
</SimpleField>
<SimpleField type="double" name="TrailLength">
<displayName><![CDATA[<i>The length in miles</i>]]></displayName>
</SimpleField>
<SimpleField type="int" name="ElevationGain">
<displayName><![CDATA[<i>change in altitude</i>]]></displayName>
</SimpleField>
</Schema> """
s = kml.Schema(ns='', id='default')
s.from_string(doc)
self.assertEqual(len(list(s.simple_fields)), 3)
self.assertEqual(list(s.simple_fields)[0]['type'], 'string')
self.assertEqual(list(s.simple_fields)[1]['type'], 'double')
self.assertEqual(list(s.simple_fields)[2]['type'], 'int')
self.assertEqual(list(s.simple_fields)[0]['name'], 'TrailHeadName')
self.assertEqual(list(s.simple_fields)[1]['name'], 'TrailLength')
self.assertEqual(list(s.simple_fields)[2]['name'], 'ElevationGain')
self.assertEqual(list(s.simple_fields)[0]['displayName'], '<b>Trail Head Name</b>')
self.assertEqual(list(s.simple_fields)[1]['displayName'], '<i>The length in miles</i>')
self.assertEqual(list(s.simple_fields)[2]['displayName'], '<i>change in altitude</i>')
s1 = kml.Schema(ns='', id='default')
s1.from_string(s.to_string())
self.assertEqual(len(list(s1.simple_fields)), 3)
self.assertEqual(list(s1.simple_fields)[0]['type'], 'string')
self.assertEqual(list(s1.simple_fields)[1]['name'], 'TrailLength')
self.assertEqual(list(s1.simple_fields)[2]['displayName'], '<i>change in altitude</i>')
self.assertEqual(s.to_string(), s1.to_string())
doc1 = """<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
%s
</Document>
</kml>""" % doc
k = kml.KML()
k.from_string(doc1)
d = list(k.features())[0]
s2 = list(d.schemata())[0]
s.ns = config.NS
self.assertEqual(s.to_string(), s2.to_string())
k1 = kml.KML()
k1.from_string(k.to_string())
self.assertEqual(k1.to_string(), k.to_string())


def test_snippet(self):
doc = """<kml xmlns="http://www.opengis.net/kml/2.2">
<Placemark>
<Snippet maxLines="2" >Short Desc</Snippet>
</Placemark> </kml>"""
k = kml.KML()
k.from_string(doc)
self.assertEqual(list(k.features())[0]._snippet['text'], 'Short Desc')
self.assertEqual(list(k.features())[0]._snippet['maxLines'], 2)
self.assertEqual(list(k.features())[0].snippet['text'], 'Short Desc')
self.assertEqual(list(k.features())[0].snippet['maxLines'], 2)
list(k.features())[0]._snippet['maxLines'] = 3
self.assertEqual(list(k.features())[0]._snippet['maxLines'], 3)
self.assertEqual(list(k.features())[0].snippet['maxLines'], 3)
self.assertTrue('maxLines="3"' in k.to_string())
list(k.features())[0]._snippet = {'text': 'Annother Snippet'}
list(k.features())[0].snippet = {'text': 'Annother Snippet'}
self.assertFalse('maxLines' in k.to_string())
self.assertTrue('Annother Snippet' in k.to_string())
list(k.features())[0]._snippet = 'Diffrent Snippet'
list(k.features())[0].snippet = 'Diffrent Snippet'
self.assertFalse('maxLines' in k.to_string())
self.assertTrue('Diffrent Snippet' in k.to_string())

Expand Down

0 comments on commit b810f3b

Please sign in to comment.