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))