From e38c8886bad4bc56a353687709284587aa4b6522 Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Sat, 14 Nov 2020 17:18:14 -0800 Subject: [PATCH 1/9] Initial read support of Track/MultiTrack --- fastkml/gx.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ fastkml/kml.py | 15 ++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index 1ddd9a66..3a4ce9b3 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -77,5 +77,74 @@ 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 xml.etree.ElementTree import Element + +from pygeoif.geometry import GeometryCollection + +from .config import etree +from .config import GXNS as NS +from .geometry import Geometry + +import fastkml.config as config + 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 + + + """ + if ns is None: + self.ns = NS + else: + self.ns = ns + super(Geometry, self).__init__(ns, id) + + 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 == ('%sMultiGeometry' % self.ns): + tracks = element.find("%sTrack" % self.ns) + if tracks: + for track in tracks: + self._get_geometry_spec(track) + geoms.append() + + if len(geoms) > 0: + geom_types = [] + for geom in geoms: + geom_types.append(geom.geom_type) + geom_types = list(set(geom_types)) + if len(geom_types) > 1: + return GeometryCollection(geoms) + if geom_types[0] == 'LineString': + return MultiLineString(geoms) + + def _get_coordinates(self, element): + coordinates = element.findall('%scoords' % self.ns) + if coordinates is not None: + coords = [coord.text.strip().split() for coord in coordinates] + return coords diff --git a/fastkml/kml.py b/fastkml/kml.py index 22321e82..801c239b 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -35,6 +35,7 @@ # from .geometry import MultiPoint, MultiLineString, MultiPolygon # from .geometry import LinearRing from .geometry import Geometry +from .gx import GxGeometry from datetime import datetime, date @@ -54,7 +55,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: @@ -1095,6 +1096,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 = GxGeometry(ns=gx.NS) + geom.from_element(track) + self._geometry = geom + return + multitrack = element.find("%sMultiTrack" % gx.NS) + if line is not None: + geom = 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))) From 287da2fabf53247345700c996ba8c6a7ecd024d8 Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Tue, 17 Nov 2020 22:25:33 -0800 Subject: [PATCH 2/9] Fixing a bunch of copypasted strings --- fastkml/gx.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index 3a4ce9b3..b0a24c86 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -110,11 +110,11 @@ def __init__( """ + super(GxGeometry, self).__init__(ns, id) if ns is None: self.ns = NS else: self.ns = ns - super(Geometry, self).__init__(ns, id) def _get_geometry(self, element): # Track @@ -126,12 +126,12 @@ def _get_geometry(self, element): def _get_multigeometry(self, element): # MultiTrack geoms = [] - if element.tag == ('%sMultiGeometry' % self.ns): - tracks = element.find("%sTrack" % self.ns) + if element.tag == ('%sMultiTrack' % self.ns): + tracks = element.findall("%sTrack" % self.ns) if tracks: for track in tracks: self._get_geometry_spec(track) - geoms.append() + geoms.append(LineString(self._get_coordinates(track))) if len(geoms) > 0: geom_types = [] @@ -144,7 +144,8 @@ def _get_multigeometry(self, element): return MultiLineString(geoms) def _get_coordinates(self, element): - coordinates = element.findall('%scoords' % self.ns) + coordinates = element.findall('%scoord' % self.ns) if coordinates is not None: - coords = [coord.text.strip().split() for coord in coordinates] + coords = [[float(c) for c in coord.text.strip().split()] + for coord in coordinates] return coords From a1bb0aa6f98f63f3ba24c3d2f39343ac10eef324 Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Tue, 17 Nov 2020 22:27:23 -0800 Subject: [PATCH 3/9] Add testcases for GxGeometry --- fastkml/test_main.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/fastkml/test_main.py b/fastkml/test_main.py index 6d149bb2..aa9c4355 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 @@ -2123,6 +2124,46 @@ def test_geometrycollection(self): self.assertEqual(len(g.geometry), 2) 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): @@ -2588,6 +2629,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)) From 114241e668c7a950be1b74811c4d8a87e5a15742 Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Mon, 23 Nov 2020 18:41:39 -0800 Subject: [PATCH 4/9] Cleaning up unneeded imports --- fastkml/gx.py | 5 ----- fastkml/kml.py | 5 ++--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index b0a24c86..cd11c30b 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -84,16 +84,11 @@ except ImportError: from pygeoif.geometry import LineString, MultiLineString -from xml.etree.ElementTree import Element - from pygeoif.geometry import GeometryCollection -from .config import etree from .config import GXNS as NS from .geometry import Geometry -import fastkml.config as config - import logging logger = logging.getLogger('fastkml.gx') diff --git a/fastkml/kml.py b/fastkml/kml.py index 801c239b..c3ca210f 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -35,7 +35,6 @@ # from .geometry import MultiPoint, MultiLineString, MultiPolygon # from .geometry import LinearRing from .geometry import Geometry -from .gx import GxGeometry from datetime import datetime, date @@ -1098,13 +1097,13 @@ def from_element(self, element): return track = element.find("%sTrack" % gx.NS) if track is not None: - geom = GxGeometry(ns=gx.NS) + 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 = GxGeometry(ns=gx.NS) + geom = gx.GxGeometry(ns=gx.NS) geom.from_element(multitrack) self._geometry = geom return From 3a0b43890a896a9c989f13b671ac95aed8364891 Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Mon, 23 Nov 2020 18:42:32 -0800 Subject: [PATCH 5/9] Line length and spacing --- fastkml/gx.py | 6 +++--- fastkml/test_main.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index cd11c30b..ca38ea90 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -97,13 +97,13 @@ class GxGeometry(Geometry): """ """ + def __init__( self, ns=None, id=None, ): """ - gxgeometry: a read-only subclass of geometry supporting gx: features, like gx:Track - - + gxgeometry: a read-only subclass of geometry supporting gx: features, + like gx:Track """ super(GxGeometry, self).__init__(ns, id) if ns is None: diff --git a/fastkml/test_main.py b/fastkml/test_main.py index aa9c4355..958290aa 100644 --- a/fastkml/test_main.py +++ b/fastkml/test_main.py @@ -2124,6 +2124,7 @@ def test_geometrycollection(self): self.assertEqual(len(g.geometry), 2) self.assertEqual(g.geometry.geom_type, 'GeometryCollection') + class GetGxGeometryTestCase(unittest.TestCase): def test_track(self): From 230982f3a1e8ea3f360067df5e4775d75b24619b Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Mon, 23 Nov 2020 18:42:42 -0800 Subject: [PATCH 6/9] Simplifying code --- fastkml/gx.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index ca38ea90..50bc4c70 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -128,11 +128,8 @@ def _get_multigeometry(self, element): self._get_geometry_spec(track) geoms.append(LineString(self._get_coordinates(track))) - if len(geoms) > 0: - geom_types = [] - for geom in geoms: - geom_types.append(geom.geom_type) - geom_types = list(set(geom_types)) + if geoms: + geom_types = list({geom.geom_type for geom in geoms}) if len(geom_types) > 1: return GeometryCollection(geoms) if geom_types[0] == 'LineString': From b7670da85ae3435dc404416379a4827a8e554232 Mon Sep 17 00:00:00 2001 From: Sourcery AI <> Date: Tue, 24 Nov 2020 02:43:37 +0000 Subject: [PATCH 7/9] 'Refactored by Sourcery' --- fastkml/gx.py | 8 +--- fastkml/kml.py | 121 ++++++++++++++++++++++--------------------------- 2 files changed, 56 insertions(+), 73 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index 50bc4c70..b80483b9 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -106,10 +106,7 @@ def __init__( like gx:Track """ super(GxGeometry, self).__init__(ns, id) - if ns is None: - self.ns = NS - else: - self.ns = ns + self.ns = NS if ns is None else ns def _get_geometry(self, element): # Track @@ -138,6 +135,5 @@ def _get_multigeometry(self, element): def _get_coordinates(self, element): coordinates = element.findall('%scoord' % self.ns) if coordinates is not None: - coords = [[float(c) for c in coord.text.strip().split()] + return [[float(c) for c in coord.text.strip().split()] for coord in coordinates] - return coords diff --git a/fastkml/kml.py b/fastkml/kml.py index c3ca210f..1d6ca110 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -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: @@ -1173,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: @@ -1292,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 @@ -1364,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): From 8e01d2d4fd77502fc2d7864c7e4a5670357bdf5a Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Sun, 29 Nov 2020 12:36:42 -0800 Subject: [PATCH 8/9] Simplify use of sets in geom_types --- fastkml/gx.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index 50bc4c70..eb3087e5 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -94,9 +94,6 @@ class GxGeometry(Geometry): - """ - - """ def __init__( self, ns=None, id=None, @@ -129,10 +126,10 @@ def _get_multigeometry(self, element): geoms.append(LineString(self._get_coordinates(track))) if geoms: - geom_types = list({geom.geom_type for geom in geoms}) + geom_types = {geom.geom_type for geom in geoms} if len(geom_types) > 1: return GeometryCollection(geoms) - if geom_types[0] == 'LineString': + if 'LineString' in geom_types: return MultiLineString(geoms) def _get_coordinates(self, element): From b81d0722a8d7025bba7fb297f7c43ed002a40e92 Mon Sep 17 00:00:00 2001 From: Evan Derickson <3587185+dericke@users.noreply.github.com> Date: Sun, 29 Nov 2020 13:44:46 -0800 Subject: [PATCH 9/9] Remove unneeded conditionals --- fastkml/gx.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index bb6fca44..bb8156ca 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -117,20 +117,20 @@ def _get_multigeometry(self, element): geoms = [] if element.tag == ('%sMultiTrack' % self.ns): tracks = element.findall("%sTrack" % self.ns) - if tracks: - for track in tracks: - self._get_geometry_spec(track) - geoms.append(LineString(self._get_coordinates(track))) - - if geoms: - 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) + 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] + return [ + [float(c) for c in coord.text.strip().split()] + for coord in coordinates + ]