Commit af5d252c authored by Christian Ledermann's avatar Christian Ledermann

comfortable timespan and timestamp handling for features

parent 7bf44fe5
...@@ -116,37 +116,100 @@ class _Feature(_BaseObject): ...@@ -116,37 +116,100 @@ class _Feature(_BaseObject):
#ScreenOverlay #ScreenOverlay
""" """
name = None name = None
#User-defined text displayed in the 3D viewer as the label for the # User-defined text displayed in the 3D viewer as the label for the
#object (for example, for a Placemark, Folder, or NetworkLink). # object (for example, for a Placemark, Folder, or NetworkLink).
description = None
#User-supplied content that appears in the description balloon.
visibility = 1 visibility = 1
#Boolean value. Specifies whether the feature is drawn in the 3D # Boolean value. Specifies whether the feature is drawn in the 3D
#viewer when it is initially loaded. In order for a feature to be # viewer when it is initially loaded. In order for a feature to be
#visible, the <visibility> tag of all its ancestors must also be # visible, the <visibility> tag of all its ancestors must also be
#set to 1. # set to 1.
isopen = 0 isopen = 0
#Boolean value. Specifies whether a Document or Folder appears # Boolean value. Specifies whether a Document or Folder appears
#closed or open when first loaded into the Places panel. # closed or open when first loaded into the Places panel.
#0=collapsed (the default), 1=expanded. # 0=collapsed (the default), 1=expanded.
#TODO atom_author = None
# KML 2.2 supports new elements for including data about the author
# and related website in your KML file. This information is displayed
# in geo search results, both in Earth browsers such as Google Earth,
# and in other applications such as Google Maps.
#TODO atom_link = None
# Specifies the URL of the website containing this KML or KMZ file.
#TODO address = None
# A string value representing an unstructured address written as a
# standard street, city, state address, and/or as a postal code.
# You can use the <address> tag to specify the location of a point
# instead of using latitude and longitude coordinates.
#TODO phoneNumber = None
# A string value representing a telephone number.
# This element is used by Google Maps Mobile only.
_snippet = None #XXX
# _snippet is eiter a tuple of a string Snippet.text and an integer
# Snippet.maxLines or a string
#
# A short description of the feature. In Google Earth, this
# description is displayed in the Places panel under the name of the
# feature. If a Snippet is not supplied, the first two lines of
# the <description> are used. In Google Earth, if a Placemark
# contains both a description and a Snippet, the <Snippet> appears
# beneath the Placemark in the Places panel, and the <description>
# appears in the Placemark's description balloon. This tag does not
# support HTML markup. <Snippet> has a maxLines attribute, an integer
# that specifies the maximum number of lines to display.
description = None
#User-supplied content that appears in the description balloon.
_styleUrl = None _styleUrl = None
#URL of a <Style> or <StyleMap> defined in a Document. # URL of a <Style> or <StyleMap> defined in a Document.
#If the style is in the same file, use a # reference. # If the style is in the same file, use a # reference.
#If the style is defined in an external file, use a full URL # If the style is defined in an external file, use a full URL
#along with # referencing. # along with # referencing.
_styles = None
_time_span = None
_time_stamp = None
#XXX atom_author = None _styles = None
#XXX atom_link = None # One or more Styles and StyleMaps can be defined to customize the
# appearance of any element derived from Feature or of the Geometry
# in a Placemark.
# A style defined within a Feature is called an "inline style" and
# applies only to the Feature that contains it. A style defined as
# the child of a <Document> is called a "shared style." A shared
# style must have an id defined for it. This id is referenced by one
# or more Features within the <Document>. In cases where a style
# element is defined both in a shared style and in an inline style
# for a Feature—that is, a Folder, GroundOverlay, NetworkLink,
# Placemark, or ScreenOverlay—the value for the Feature's inline
# style takes precedence over the value for the shared style.
_time_span = None #XXX
# Associates this Feature with a period of time.
_time_stamp = None #XXX
# Associates this Feature with a point in time.
#TODO Region = None
# Features and geometry associated with a Region are drawn only when
# the Region is active.
#TODO ExtendedData = None
# Allows you to add custom data to a KML file. This data can be
# (1) data that references an external XML schema,
# (2) untyped data/value pairs, or
# (3) typed data.
# A given KML Feature can contain a combination of these types of
# custom data.
#
# <Metadata> (deprecated in KML 2.2; use <ExtendedData> instead)
def __init__(self, ns=None, id=None, name=None, description=None, def __init__(self, ns=None, id=None, name=None, description=None,
styles=None, styleUrl=None): styles=None, styleUrl=None):
super(_Feature, self).__init__(ns, id) super(_Feature, self).__init__(ns, id)
self.name=name self.name=name
self.description=description self.description=description
if styleUrl is not None:
self.styleUrl = styleUrl self.styleUrl = styleUrl
self._styles = [] self._styles = []
if styles: if styles:
...@@ -156,7 +219,7 @@ class _Feature(_BaseObject): ...@@ -156,7 +219,7 @@ class _Feature(_BaseObject):
@property @property
def styleUrl(self): def styleUrl(self):
""" Returns the url only, not a full StyleUrl object. """ Returns the url only, not a full StyleUrl object.
if you need the full StyleUrl use _styleUrl """ if you need the full StyleUrl object use _styleUrl """
if isinstance(self._styleUrl, StyleUrl): if isinstance(self._styleUrl, StyleUrl):
return self._styleUrl.url return self._styleUrl.url
...@@ -173,7 +236,57 @@ class _Feature(_BaseObject): ...@@ -173,7 +236,57 @@ class _Feature(_BaseObject):
else: else:
raise ValueError raise ValueError
@property
def timeStamp(self):
""" This just returns the datetime portion of the timestamp"""
if self._time_stamp is not None:
return self._time_stamp.timestamp[0]
@timeStamp.setter
def timeStamp(self, dt):
if dt == None:
self._time_stamp = None
else:
self._time_stamp = TimeStamp(timestamp=dt)
if self._time_span is not None:
logger.warn('Setting a TimeStamp, TimeSpan deleted')
self._time_span = None
@property
def begin(self):
if self._time_span is not None:
return self._time_span.begin[0]
@begin.setter
def begin(self, dt):
if self._time_span is None:
self._time_span = TimeSpan(begin=dt)
else:
if self._time_span.begin is None:
self._time_span.begin = [dt, None]
else:
self._time_span.begin[0] = dt
if self._time_stamp is not None:
logger.warn('Setting a TimeSpan, TimeStamp deleted')
self._time_stamp = None
@property
def end(self):
if self._time_span is not None:
return self._time_span.end[0]
@end.setter
def end(self, dt):
if self._time_span is None:
self._time_span = TimeStamp(end=dt)
else:
if self._time_span.end is None:
self._time_span.end = [dt, None]
else:
self._time_span.end[0] = dt
if self._time_stamp is not None:
logger.warn('Setting a TimeSpan, TimeStamp deleted')
self._time_stamp = None
def append_style(self, style): def append_style(self, style):
""" append a style to the feature """ """ append a style to the feature """
...@@ -201,17 +314,28 @@ class _Feature(_BaseObject): ...@@ -201,17 +314,28 @@ class _Feature(_BaseObject):
description.text = self.description description.text = self.description
visibility = etree.SubElement(element, "%svisibility" %self.ns) visibility = etree.SubElement(element, "%svisibility" %self.ns)
visibility.text = str(self.visibility) visibility.text = str(self.visibility)
if self.isopen:
isopen = etree.SubElement(element, "%sopen" %self.ns) isopen = etree.SubElement(element, "%sopen" %self.ns)
isopen.text = str(self.isopen) isopen.text = str(self.isopen)
if self._styleUrl is not None: if self._styleUrl is not None:
element.append(self._styleUrl.etree_element()) element.append(self._styleUrl.etree_element())
for style in self.styles(): for style in self.styles():
element.append(style.etree_element()) element.append(style.etree_element())
if self._snippet:
snippet = etree.SubElement(element, "%sSnippet" %self.ns)
if isinstance(self._snippet, basestring):
snippet.text = self._snippet
else:
assert (isinstance(self._snippet[0], basestring))
snippet.text = self._snippet[0]
if self._snippet[1]:
snippet.set('maxLines', str(self._snippet[1]))
if (self._time_span is not None) and (self._time_stamp is not None): if (self._time_span is not None) and (self._time_stamp is not None):
raise ValueError raise ValueError('Either Timestamp or Timespan can be defined, not both')
#elif self._time_span: elif self._time_span:
# timespan = TimeStamp(self.ns, begin=self._time_span[0][0], element.append(self._time_span.etree_element())
# begin_res=self._time_span[0][1]) elif self._time_stamp:
element.append(self._time_stamp.etree_element())
return element return element
...@@ -250,7 +374,23 @@ class _Feature(_BaseObject): ...@@ -250,7 +374,23 @@ class _Feature(_BaseObject):
s = StyleUrl(self.ns) s = StyleUrl(self.ns)
s.from_element(style_url) s.from_element(style_url)
self._styleUrl = s self._styleUrl = s
#XXX Timespan/stamp snippet = element.find('%sSnippet' % self.ns)
if snippet is not None:
self._snippet = ('',0)
self._snippet[0] = snippet.text
if snippet.get('maxLines'):
self._snippet[1] = int(snippet.get('maxLines'))
timespan = element.find('%sTimeSpan' % self.ns)
if timespan is not None:
s = TimeSpan()
s.from_element(timespan)
self._time_span = s
timestamp = element.find('%sTimeStamp' % self.ns)
if timestamp is not None:
s = TimeStamp()
s.from_element(timestamp)
self._time_stamp = s
...@@ -529,9 +669,25 @@ class _TimePrimitive(_BaseObject): ...@@ -529,9 +669,25 @@ class _TimePrimitive(_BaseObject):
RESOLUTIONS = ['gYear', 'gYearMonth', 'date', 'dateTime'] RESOLUTIONS = ['gYear', 'gYearMonth', 'date', 'dateTime']
def get_resolution(self, dt, resolution):
if resolution:
if resolution not in self.RESOLUTIONS:
raise ValueError
else:
return resolution
else:
if isinstance(dt, datetime):
resolution = 'dateTime'
elif isinstance(dt, date):
resolution = 'date'
else:
resolution = None
return resolution
def parse_str(self, datestr): def parse_str(self, datestr):
resolution = 'dateTime' resolution = 'dateTime'
year = 1900 year = 0
month = 1 month = 1
day = 1 day = 1
if len(datestr) == 4: if len(datestr) == 4:
...@@ -556,20 +712,12 @@ class _TimePrimitive(_BaseObject): ...@@ -556,20 +712,12 @@ class _TimePrimitive(_BaseObject):
dt = dateutil.parser.parse(datestr) dt = dateutil.parser.parse(datestr)
else: else:
raise ValueError raise ValueError
return dt, resolution return [dt, resolution]
def date_to_string(self, dt, resolution=None): def date_to_string(self, dt, resolution=None):
if resolution: if isinstance(dt, (date, datetime)):
if resolution not in self.RESOLUTIONS: resolution = self.get_resolution(dt, resolution)
raise ValueError
else:
if isinstance(dt, datetime):
resolution = 'dateTime'
elif isinstance(dt, date):
resolution = 'date'
else:
raise ValueError
if resolution == 'gYear': if resolution == 'gYear':
return dt.strftime('%Y') return dt.strftime('%Y')
elif resolution == 'gYearMonth': elif resolution == 'gYearMonth':
...@@ -578,7 +726,7 @@ class _TimePrimitive(_BaseObject): ...@@ -578,7 +726,7 @@ class _TimePrimitive(_BaseObject):
if isinstance(dt, datetime): if isinstance(dt, datetime):
return dt.date().isoformat() return dt.date().isoformat()
else: else:
dt.isoformat() return dt.isoformat()
elif resolution == 'dateTime': elif resolution == 'dateTime':
return dt.isoformat() return dt.isoformat()
...@@ -588,10 +736,10 @@ class TimeStamp(_TimePrimitive): ...@@ -588,10 +736,10 @@ class TimeStamp(_TimePrimitive):
__name__ = 'TimeStamp' __name__ = 'TimeStamp'
timestamp = None timestamp = None
def __init__(self, ns=None, id=None, timestamp=None, resolution='dateTime'): def __init__(self, ns=None, id=None, timestamp=None, resolution=None):
super(TimeStamp, self).__init__(ns, id) super(TimeStamp, self).__init__(ns, id)
if timestamp: resolution = self.get_resolution(timestamp, resolution)
self.timestamp = (timestamp, resolution) self.timestamp = [timestamp, resolution]
def etree_element(self): def etree_element(self):
element = super(TimeStamp, self).etree_element() element = super(TimeStamp, self).etree_element()
...@@ -614,13 +762,15 @@ class TimeSpan(_TimePrimitive): ...@@ -614,13 +762,15 @@ class TimeSpan(_TimePrimitive):
begin = None begin = None
end = None end = None
def __init__(self, ns=None, id=None, begin=None, begin_res='dateTime', def __init__(self, ns=None, id=None, begin=None, begin_res=None,
end=None, end_res='dateTime'): end=None, end_res=None):
super(TimeStamp, self).__init__(ns, id) super(TimeSpan, self).__init__(ns, id)
if begin: if begin:
self.begin = (begin, begin_res) resolution = self.get_resolution(begin, begin_res)
self.begin = [begin, resolution]
if end: if end:
self.end = (end, end_res) resolution = self.get_resolution(end, end_res)
self.end = [end, resolution]
def from_element(self, element): def from_element(self, element):
super(TimeSpan, self).from_element(element) super(TimeSpan, self).from_element(element)
...@@ -634,8 +784,16 @@ class TimeSpan(_TimePrimitive): ...@@ -634,8 +784,16 @@ class TimeSpan(_TimePrimitive):
def etree_element(self): def etree_element(self):
element = super(TimeSpan, self).etree_element() element = super(TimeSpan, self).etree_element()
if self.begin is not None: if self.begin is not None:
text = self.date_to_string(*self.begin)
if text:
begin = etree.SubElement(element, "%sbegin" %self.ns) begin = etree.SubElement(element, "%sbegin" %self.ns)
begin.text = self.date_to_string(*self.begin) begin.text = text
if self.end is not None: if self.end is not None:
text = self.date_to_string(*self.end)
if text:
end = etree.SubElement(element, "%send" %self.ns) end = etree.SubElement(element, "%send" %self.ns)
end.text = self.date_to_string(*self.end) end.text = text
if self.begin == self.end == None:
raise ValueError("Either begin, end or both must be set")
#TODO test if end > begin
return element
...@@ -34,8 +34,7 @@ class StyleUrl(_BaseObject): ...@@ -34,8 +34,7 @@ class StyleUrl(_BaseObject):
element.text = self.url element.text = self.url
return element return element
else: else:
logger.critical('No url given for styleUrl') raise ValueError('No url given for styleUrl')
raise ValueError
def from_element(self, element): def from_element(self, element):
super(StyleUrl, self).from_element(element) super(StyleUrl, self).from_element(element)
...@@ -79,7 +78,7 @@ class Style(_StyleSelector): ...@@ -79,7 +78,7 @@ class Style(_StyleSelector):
def styles(self): def styles(self):
for style in self._styles: for style in self._styles:
if isinstance(style, _ColorStyle): if isinstance(style, (_ColorStyle, BalloonStyle)):
yield style yield style
else: else:
raise TypeError raise TypeError
...@@ -106,6 +105,7 @@ class Style(_StyleSelector): ...@@ -106,6 +105,7 @@ class Style(_StyleSelector):
thestyle = LabelStyle(self.ns) thestyle = LabelStyle(self.ns)
thestyle.from_element(style) thestyle.from_element(style)
self.append_style(thestyle) self.append_style(thestyle)
#XXX BalloonStyle
def etree_element(self): def etree_element(self):
element = super(Style, self).etree_element() element = super(Style, self).etree_element()
...@@ -373,4 +373,7 @@ class LabelStyle(_ColorStyle): ...@@ -373,4 +373,7 @@ class LabelStyle(_ColorStyle):
self.scale = float(scale.text) self.scale = float(scale.text)
class BalloonStyle(_BaseObject): class BalloonStyle(_BaseObject):
""" Specifies how the description balloon for placemarks is drawn.
The <bgColor>, if specified, is used as the background color of
the balloon."""
pass pass
...@@ -5,6 +5,7 @@ from fastkml import kml ...@@ -5,6 +5,7 @@ from fastkml import kml
from fastkml import styles from fastkml import styles
from fastkml import base from fastkml import base
from fastkml import config from fastkml import config
import datetime
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from shapely.geometry import Point, LineString, Polygon from shapely.geometry import Point, LineString, Polygon
from shapely.geometry import MultiPoint, MultiLineString, MultiPolygon from shapely.geometry import MultiPoint, MultiLineString, MultiPolygon
...@@ -43,8 +44,28 @@ class BaseClassesTestCase(unittest.TestCase): ...@@ -43,8 +44,28 @@ class BaseClassesTestCase(unittest.TestCase):
def test_Feature(self): def test_Feature(self):
f = kml._Feature(name='A Feature') f = kml._Feature(name='A Feature')
self.assertRaises(NotImplementedError, f.etree_element)
self.assertEqual(f.name, 'A Feature')
self.assertEqual(f.visibility, 1)
self.assertEqual(f.isopen, 0)
#self.assertEqual(f.atom_author, None)
#self.assertEqual(f.atom_link, None)
#self.assertEqual(f.address, None)
#self.assertEqual(f.phoneNumber, None)
self.assertEqual(f._snippet, None)
self.assertEqual(f.description, None)
self.assertEqual(f._styleUrl, None)
self.assertEqual(f._styles, [])
self.assertEqual(f._time_span, None)
self.assertEqual(f._time_stamp, None)
#self.assertEqual(f.region, None)
#self.assertEqual(f.extended_data, None)
f.__name__ = 'Feature'
f.styleUrl = '#default' f.styleUrl = '#default'
pass self.assertTrue('Feature>' in f.to_string())
self.assertTrue('#default' in f.to_string())
def test_Container(self): def test_Container(self):
pass pass
...@@ -631,12 +652,122 @@ class StyleFromStringTestCase( unittest.TestCase ): ...@@ -631,12 +652,122 @@ class StyleFromStringTestCase( unittest.TestCase ):
k2.from_string(k.to_string()) k2.from_string(k.to_string())
self.assertEqual(k.to_string(), k2.to_string()) self.assertEqual(k.to_string(), k2.to_string())
class DateTimeTestCase( unittest.TestCase ):
def test_timestamp(self):
now = datetime.datetime.now()
ts = kml.TimeStamp(timestamp=now)
self.assertEqual(ts.timestamp, [now, 'dateTime'])
self.assertTrue('TimeStamp>' in ts.to_string())
self.assertTrue('when>' in ts.to_string())
self.assertTrue(now.isoformat() in ts.to_string())
y2k = datetime.date(2000,1,1)
ts = kml.TimeStamp(timestamp=y2k)
self.assertEqual(ts.timestamp, [y2k, 'date'])
self.assertTrue('2000-01-01' in ts.to_string())
def test_timestamp_resolution(self):
now = datetime.datetime.now()
ts = kml.TimeStamp(timestamp=now)
self.assertTrue(now.isoformat() in ts.to_string())
ts.timestamp[1] = 'date'
self.assertTrue(now.date().isoformat() in ts.to_string())
self.assertFalse(now.isoformat() in ts.to_string())
year = str(now.year)
ym = now.strftime('%Y-%m')
ts.timestamp[1] = 'gYearMonth'
self.assertTrue(ym in ts.to_string())
self.assertFalse(now.date().isoformat() in ts.to_string())
ts.timestamp[1] = 'gYear'
self.assertTrue(year in ts.to_string())
self.assertFalse(ym in ts.to_string())
ts.timestamp = None
self.assertRaises(TypeError, ts.to_string)
def test_timespan(self):
now = datetime.datetime.now()
y2k = datetime.datetime(2000,1,1)
ts = kml.TimeSpan(end=now, begin=y2k)
self.assertEqual(ts.end, [now, 'dateTime'])
self.assertEqual(ts.begin, [y2k, 'dateTime'])
self.assertTrue('TimeSpan>' in ts.to_string())
self.assertTrue('begin>' in ts.to_string())
self.assertTrue('end>' in ts.to_string())
self.assertTrue(now.isoformat() in ts.to_string())
self.assertTrue(y2k.isoformat() in ts.to_string())
ts.end = None
self.assertFalse(now.isoformat() in ts.to_string())
self.assertTrue(y2k.isoformat() in ts.to_string())
ts.begin = None
self.assertRaises(ValueError, ts.to_string)
def test_feature_timestamp(self):
now = datetime.datetime.now()
f = kml.Document()
f.timeStamp = now
self.assertTrue(now.isoformat() in f.to_string())
self.assertTrue('TimeStamp>' in f.to_string())
self.assertTrue('when>' in f.to_string())
f.timeStamp = now.date()
self.assertTrue(now.date().isoformat() in f.to_string())
self.assertFalse(now.isoformat() in f.to_string())
f.timeStamp = None
self.assertFalse('TimeStamp>' in f.to_string())
def test_feature_timespan(self):
now = datetime.datetime.now()
y2k = datetime.date(2000,1,1)
f = kml.Document()
f.begin = y2k
f.end = now
self.assertTrue(now.isoformat() in f.to_string())
self.assertTrue('2000-01-01' in f.to_string())
self.assertTrue('TimeSpan>' in f.to_string())
self.assertTrue('begin>' in f.to_string())
self.assertTrue('end>' in f.to_string())
f.end = None
self.assertFalse(now.isoformat() in f.to_string())
self.assertTrue('2000-01-01' in f.to_string())
self.assertTrue('TimeSpan>' in f.to_string())
self.assertTrue('begin>' in f.to_string())
self.assertFalse('end>' in f.to_string())
f.begin = None
self.assertFalse('TimeSpan>' in f.to_string())
def test_feature_timespan_stamp(self):
now = datetime.datetime.now()
y2k = datetime.date(2000,1,1)
f = kml.Document()
f.begin = y2k
f.end = now
self.assertTrue(now.isoformat() in f.to_string())
self.assertTrue('2000-01-01' in f.to_string())
self.assertTrue('TimeSpan>' in f.to_string())
self.assertTrue('begin>' in f.to_string())
self.assertTrue('end>' in f.to_string())
self.assertFalse('TimeStamp>' in f.to_string())
self.assertFalse('when>' in f.to_string())
f.timeStamp = now
self.assertTrue(now.isoformat() in f.to_string())
self.assertTrue('TimeStamp>' in f.to_string())
self.assertTrue('when>' in f.to_string())
self.assertFalse('2000-01-01' in f.to_string())
self.assertFalse('TimeSpan>' in f.to_string())
self.assertFalse('begin>' in f.to_string())
self.assertFalse('end>' in f.to_string())
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite( KmlFromStringTestCase )) suite.addTest(unittest.makeSuite( KmlFromStringTestCase ))
suite.addTest(unittest.makeSuite( BuildKmlTestCase )) suite.addTest(unittest.makeSuite( BuildKmlTestCase ))
suite.addTest(unittest.makeSuite( StyleFromStringTestCase )) suite.addTest(unittest.makeSuite( StyleFromStringTestCase ))
suite.addTest(unittest.makeSuite(BaseClassesTestCase)) suite.addTest(unittest.makeSuite(BaseClassesTestCase))
suite.addTest(unittest.makeSuite(DateTimeTestCase))
return suite return suite
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment