Skip to content

Commit

Permalink
Merge pull request #92 from dericke/gx_track
Browse files Browse the repository at this point in the history
gx:Track/MultiTrack read support
  • Loading branch information
cleder authored Dec 3, 2020
2 parents 7567c2e + b81d072 commit 5d2ef77
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 69 deletions.
55 changes: 55 additions & 0 deletions fastkml/gx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
135 changes: 67 additions & 68 deletions fastkml/kml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
45 changes: 44 additions & 1 deletion fastkml/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -2124,6 +2125,47 @@ def test_geometrycollection(self):
self.assertEqual(g.geometry.geom_type, 'GeometryCollection')


class GetGxGeometryTestCase(unittest.TestCase):

def test_track(self):
doc = """<gx:Track xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
<when>2020-01-01T00:00:00Z</when>
<when>2020-01-01T00:10:00Z</when>
<gx:coord>0.000000 0.000000</gx:coord>
<gx:coord>1.000000 1.000000</gx:coord>
</gx:Track>"""

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 = """
<gx:MultiTrack xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
<gx:Track>
<when>2020-01-01T00:00:00Z</when>
<when>2020-01-01T00:10:00Z</when>
<gx:coord>0.000000 0.000000</gx:coord>
<gx:coord>1.000000 0.000000</gx:coord>
</gx:Track>
<gx:Track>
<when>2020-01-01T00:10:00Z</when>
<when>2020-01-01T00:20:00Z</when>
<gx:coord>0.000000 1.000000</gx:coord>
<gx:coord>1.000000 1.000000</gx:coord>
</gx:Track>
</gx:MultiTrack>
"""

g = GxGeometry()
g.from_string(doc)
self.assertEqual(len(g.geometry), 2)


class Force3DTestCase(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 5d2ef77

Please sign in to comment.