diff --git a/fastkml/gx.py b/fastkml/gx.py
index 1ddd9a66..bb8156ca 100644
--- a/fastkml/gx.py
+++ b/fastkml/gx.py
@@ -77,5 +77,60 @@
located at http://developers.google.com/kml/schema/kml22gx.xsd.
"""
+try:
+ from shapely.geometry.linestring import LineString
+ from shapely.geometry.multilinestring import MultiLineString
+
+except ImportError:
+ from pygeoif.geometry import LineString, MultiLineString
+
+from pygeoif.geometry import GeometryCollection
+
+from .config import GXNS as NS
+from .geometry import Geometry
+
import logging
logger = logging.getLogger('fastkml.gx')
+
+
+class GxGeometry(Geometry):
+
+ def __init__(
+ self, ns=None, id=None,
+ ):
+ """
+ gxgeometry: a read-only subclass of geometry supporting gx: features,
+ like gx:Track
+ """
+ super(GxGeometry, self).__init__(ns, id)
+ self.ns = NS if ns is None else ns
+
+ def _get_geometry(self, element):
+ # Track
+ if element.tag == ('%sTrack' % self.ns):
+ coords = self._get_coordinates(element)
+ self._get_geometry_spec(element)
+ return LineString(coords)
+
+ def _get_multigeometry(self, element):
+ # MultiTrack
+ geoms = []
+ if element.tag == ('%sMultiTrack' % self.ns):
+ tracks = element.findall("%sTrack" % self.ns)
+ for track in tracks:
+ self._get_geometry_spec(track)
+ geoms.append(LineString(self._get_coordinates(track)))
+
+ geom_types = {geom.geom_type for geom in geoms}
+ if len(geom_types) > 1:
+ return GeometryCollection(geoms)
+ if 'LineString' in geom_types:
+ return MultiLineString(geoms)
+
+ def _get_coordinates(self, element):
+ coordinates = element.findall('%scoord' % self.ns)
+ if coordinates is not None:
+ return [
+ [float(c) for c in coord.text.strip().split()]
+ for coord in coordinates
+ ]
diff --git a/fastkml/kml.py b/fastkml/kml.py
index 22321e82..1d6ca110 100644
--- a/fastkml/kml.py
+++ b/fastkml/kml.py
@@ -54,7 +54,7 @@
from .styles import StyleUrl, Style, StyleMap, _StyleSelector
import fastkml.atom as atom
-# import fastkml.gx as gx
+import fastkml.gx as gx
import fastkml.config as config
try:
@@ -78,10 +78,7 @@ def __init__(self, ns=None):
"""
self._features = []
- if ns is None:
- self.ns = config.KMLNS
- else:
- self.ns = ns
+ self.ns = config.KMLNS if ns is None else ns
def from_string(self, xml_string):
""" create a KML object from a xml string"""
@@ -93,26 +90,26 @@ def from_string(self, xml_string):
else:
element = etree.XML(xml_string)
- if element.tag.endswith('kml'):
- ns = element.tag.rstrip('kml')
- documents = element.findall('%sDocument' % ns)
- for document in documents:
- feature = Document(ns)
- feature.from_element(document)
- self.append(feature)
- folders = element.findall('%sFolder' % ns)
- for folder in folders:
- feature = Folder(ns)
- feature.from_element(folder)
- self.append(feature)
- placemarks = element.findall('%sPlacemark' % ns)
- for placemark in placemarks:
- feature = Placemark(ns)
- feature.from_element(placemark)
- self.append(feature)
- else:
+ if not element.tag.endswith('kml'):
raise TypeError
+ ns = element.tag.rstrip('kml')
+ documents = element.findall('%sDocument' % ns)
+ for document in documents:
+ feature = Document(ns)
+ feature.from_element(document)
+ self.append(feature)
+ folders = element.findall('%sFolder' % ns)
+ for folder in folders:
+ feature = Folder(ns)
+ feature.from_element(folder)
+ self.append(feature)
+ placemarks = element.findall('%sPlacemark' % ns)
+ for placemark in placemarks:
+ feature = Placemark(ns)
+ feature.from_element(placemark)
+ self.append(feature)
+
def etree_element(self):
# self.ns may be empty, which leads to unprefixed kml elements.
# However, in this case the xlmns should still be mentioned on the kml
@@ -309,10 +306,7 @@ def timeStamp(self):
@timeStamp.setter
def timeStamp(self, dt):
- if dt is None:
- self._time_stamp = None
- else:
- self._time_stamp = TimeStamp(timestamp=dt)
+ self._time_stamp = None if dt is None else TimeStamp(timestamp=dt)
if self._time_span is not None:
logger.warn('Setting a TimeStamp, TimeSpan deleted')
self._time_span = None
@@ -404,24 +398,25 @@ def styles(self):
@property
def snippet(self):
- if self._snippet:
- if isinstance(self._snippet, dict):
- text = self._snippet.get('text')
- if text:
- assert (isinstance(text, basestring))
- max_lines = self._snippet.get('maxLines', None)
- if max_lines is None:
- return {'text': text}
- elif int(max_lines) > 0:
- # if maxLines <=0 ignore it
- return {'text': text, 'maxLines': max_lines}
- elif isinstance(self._snippet, basestring):
- return self._snippet
- else:
- raise ValueError(
- "Snippet must be dict of "
- "{'text':t, 'maxLines':i} or string"
- )
+ if not self._snippet:
+ return
+ if isinstance(self._snippet, dict):
+ text = self._snippet.get('text')
+ if text:
+ assert (isinstance(text, basestring))
+ max_lines = self._snippet.get('maxLines', None)
+ if max_lines is None:
+ return {'text': text}
+ elif int(max_lines) > 0:
+ # if maxLines <=0 ignore it
+ return {'text': text, 'maxLines': max_lines}
+ elif isinstance(self._snippet, basestring):
+ return self._snippet
+ else:
+ raise ValueError(
+ "Snippet must be dict of "
+ "{'text':t, 'maxLines':i} or string"
+ )
@snippet.setter
def snippet(self, snip=None):
@@ -526,16 +521,10 @@ def from_element(self, element):
self.description = description.text
visibility = element.find('%svisibility' % self.ns)
if visibility is not None:
- if visibility.text in ['1', 'true']:
- self.visibility = 1
- else:
- self.visibility = 0
+ self.visibility = 1 if visibility.text in ['1', 'true'] else 0
isopen = element.find('%sopen' % self.ns)
if isopen is not None:
- if isopen.text in ['1', 'true']:
- self.isopen = 1
- else:
- self.isopen = 0
+ self.isopen = 1 if isopen.text in ['1', 'true'] else 0
styles = element.findall('%sStyle' % self.ns)
for style in styles:
s = Style(self.ns)
@@ -971,8 +960,7 @@ class Document(_Container):
def schemata(self):
if self._schemata:
- for schema in self._schemata:
- yield schema
+ yield from self._schemata
def append_schema(self, schema):
if self._schemata is None:
@@ -1095,6 +1083,18 @@ def from_element(self, element):
geom.from_element(multigeometry)
self._geometry = geom
return
+ track = element.find("%sTrack" % gx.NS)
+ if track is not None:
+ geom = gx.GxGeometry(ns=gx.NS)
+ geom.from_element(track)
+ self._geometry = geom
+ return
+ multitrack = element.find("%sMultiTrack" % gx.NS)
+ if line is not None:
+ geom = gx.GxGeometry(ns=gx.NS)
+ geom.from_element(multitrack)
+ self._geometry = geom
+ return
logger.warn('No geometries found')
logger.debug(u'Problem with element: {}'.format(etree.tostring(element)))
@@ -1161,7 +1161,7 @@ def parse_str(self, datestr):
year = int(datestr.split('-')[0])
month = int(datestr.split('-')[1])
dt = datetime(year, month, day)
- elif len(datestr) == 8 or len(datestr) == 10:
+ elif len(datestr) in [8, 10]:
resolution = 'date'
dt = dateutil.parser.parse(datestr)
elif len(datestr) > 10:
@@ -1280,14 +1280,16 @@ def __init__(self, ns=None, id=None, name=None, fields=None):
@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')
- })
+ sfs = [
+ {
+ 'type': simple_field['type'],
+ 'name': simple_field['name'],
+ 'displayName': simple_field.get('displayName'),
+ }
+ for simple_field in self._simple_fields
+ if simple_field.get('type') and simple_field.get('name')
+ ]
+
return tuple(sfs)
@simple_fields.setter
@@ -1352,10 +1354,7 @@ def from_element(self, element):
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
+ sfdisplay_name = display_name.text if display_name is not None else None
self.append(sftype, sfname, sfdisplay_name)
def etree_element(self):
diff --git a/fastkml/test_main.py b/fastkml/test_main.py
index 6d149bb2..958290aa 100644
--- a/fastkml/test_main.py
+++ b/fastkml/test_main.py
@@ -25,7 +25,6 @@
from fastkml import base
from fastkml import atom
from fastkml import config
-from fastkml import gx # NOQA
import datetime
from dateutil.tz import tzutc, tzoffset
@@ -37,6 +36,8 @@
from fastkml.geometry import LinearRing, GeometryCollection
from fastkml.geometry import Geometry
+from fastkml.gx import GxGeometry
+
class BaseClassesTestCase(unittest.TestCase):
""" BaseClasses must raise a NotImplementedError on etree_element
@@ -2124,6 +2125,47 @@ def test_geometrycollection(self):
self.assertEqual(g.geometry.geom_type, 'GeometryCollection')
+class GetGxGeometryTestCase(unittest.TestCase):
+
+ def test_track(self):
+ doc = """
+ 2020-01-01T00:00:00Z
+ 2020-01-01T00:10:00Z
+ 0.000000 0.000000
+ 1.000000 1.000000
+ """
+
+ g = GxGeometry()
+ g.from_string(doc)
+ self.assertEqual(
+ g.geometry.__geo_interface__,
+ {
+ 'type': 'LineString',
+ 'coordinates': ((0.0, 0.0), (1.0, 1.0))})
+
+ def test_multitrack(self):
+ doc = """
+
+
+ 2020-01-01T00:00:00Z
+ 2020-01-01T00:10:00Z
+ 0.000000 0.000000
+ 1.000000 0.000000
+
+
+ 2020-01-01T00:10:00Z
+ 2020-01-01T00:20:00Z
+ 0.000000 1.000000
+ 1.000000 1.000000
+
+
+ """
+
+ g = GxGeometry()
+ g.from_string(doc)
+ self.assertEqual(len(g.geometry), 2)
+
+
class Force3DTestCase(unittest.TestCase):
def setUp(self):
@@ -2588,6 +2630,7 @@ def test_suite():
suite.addTest(unittest.makeSuite(AtomTestCase))
suite.addTest(unittest.makeSuite(SetGeometryTestCase))
suite.addTest(unittest.makeSuite(GetGeometryTestCase))
+ suite.addTest(unittest.makeSuite(GetGxGeometryTestCase))
suite.addTest(unittest.makeSuite(Force3DTestCase))
suite.addTest(unittest.makeSuite(BaseOverlayTestCase))
suite.addTest(unittest.makeSuite(GroundOverlayTestCase))