From 5fd49f8bb72943b54ddb2b0bb5235be3b0adc20c Mon Sep 17 00:00:00 2001 From: Kyle Linden Date: Tue, 31 Oct 2017 10:29:10 -0400 Subject: [PATCH 001/144] allow Overlays to be added to the document and folders --- fastkml/kml.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 22321e82..b46e1550 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -147,21 +147,21 @@ def to_string(self, prettyprint=False): def features(self): """ iterate over features """ for feature in self._features: - if isinstance(feature, (Document, Folder, Placemark)): + if isinstance(feature, (Document, Folder, Placemark, _Overlay)): yield feature else: raise TypeError( "Features must be instances of " - "(Document, Folder, Placemark)" + "(Document, Folder, Placemark, Overlay)" ) def append(self, kmlobj): """ append a feature """ - if isinstance(kmlobj, (Document, Folder, Placemark)): + if isinstance(kmlobj, (Document, Folder, Placemark, _Overlay)): self._features.append(kmlobj) else: raise TypeError( - "Features must be instances of (Document, Folder, Placemark)") + "Features must be instances of (Document, Folder, Placemark, Overlay)") class _Feature(_BaseObject): @@ -618,12 +618,12 @@ def __init__( def features(self): """ iterate over features """ for feature in self._features: - if isinstance(feature, (Folder, Placemark, Document)): + if isinstance(feature, (Folder, Placemark, Document, _Overlay)): yield feature else: raise TypeError( "Features must be instances of " - "(Folder, Placemark, Document)" + "(Folder, Placemark, Document, Overlay)" ) def etree_element(self): @@ -634,12 +634,12 @@ def etree_element(self): def append(self, kmlobj): """ append a feature """ - if isinstance(kmlobj, (Folder, Placemark, Document)): + if isinstance(kmlobj, (Folder, Placemark, Document, _Overlay)): self._features.append(kmlobj) else: raise TypeError( "Features must be instances of " - "(Folder, Placemark, Document)" + "(Folder, Placemark, Document, Overlay)" ) assert(kmlobj != self) From ad8d8e40f66618827764faf9152d7e13de66d8d5 Mon Sep 17 00:00:00 2001 From: Kyle Linden Date: Tue, 31 Oct 2017 16:34:41 -0400 Subject: [PATCH 002/144] added class for Overlay icons --- fastkml/kml.py | 203 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 192 insertions(+), 11 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index b46e1550..d7f87aff 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -711,14 +711,10 @@ def icon(self): return self._icon @icon.setter - def icon(self, url): - if isinstance(url, basestring): - if not url.startswith(''): - url = '' + url - if not url.endswith(''): - url = url + '' - self._icon = url - elif url is None: + def icon(self, value): + if isinstance(value, Icon): + self._icon = value + elif value is None: self._icon = None else: raise ValueError @@ -732,8 +728,7 @@ def etree_element(self): drawOrder = etree.SubElement(element, "%sdrawOrder" % self.ns) drawOrder.text = self._drawOrder if self._icon: - icon = etree.SubElement(element, "%sicon" % self.ns) - icon.text = self._icon + element.append(self._icon.etree_element()) return element def from_element(self, element): @@ -746,7 +741,9 @@ def from_element(self, element): self.drawOrder = drawOrder.text icon = element.find('%sicon' % self.ns) if icon is not None: - self.icon = icon.text + s = Icon(self.ns) + s.from_element(icon) + self._icon = s class GroundOverlay(_Overlay): @@ -1524,3 +1521,187 @@ def from_element(self, element): simple_data = element.findall('%sSimpleData' % self.ns) for sd in simple_data: self.append_data(sd.get('name'), sd.text) + + +class Icon(_BaseObject): + '''Represents an element used in Overlays ''' + + __name__ = "Icon" + + _href = None + _refresh_mode = None + _refresh_interval = None + _view_refresh_mode = None + _view_refresh_time = None + _view_bound_scale = None + _view_format = None + _http_query = None + + @property + def href(self): + return self._href + + @href.setter + def href(self, href): + if isinstance(href, basestring): + self._href = href + elif href is None: + self._href = None + else: + raise ValueError + + @property + def refresh_mode(self): + return self._refresh_mode + + @refresh_mode.setter + def refresh_mode(self, refresh_mode): + if isinstance(refresh_mode, basestring): + self._refresh_mode = refresh_mode + elif refresh_mode is None: + self._refresh_mode = None + else: + raise ValueError + + @property + def refresh_interval(self): + return self._refresh_interval + + @refresh_interval.setter + def refresh_interval(self, refresh_interval): + if isinstance(refresh_interval, basestring): + self._refresh_interval = refresh_interval + elif refresh_interval is None: + self._refresh_interval = None + else: + raise ValueError + + @property + def view_refresh_mode(self): + return self._view_refresh_mode + + @view_refresh_mode.setter + def view_refresh_mode(self, view_refresh_mode): + if isinstance(view_refresh_mode, basestring): + self._view_refresh_mode = view_refresh_mode + elif view_refresh_mode is None: + self._view_refresh_mode = None + else: + raise ValueError + + @property + def view_refresh_time(self): + return self._view_refresh_time + + @view_refresh_time.setter + def view_refresh_time(self, view_refresh_time): + if isinstance(view_refresh_time, basestring): + self._view_refresh_time = view_refresh_time + elif view_refresh_time is None: + self._view_refresh_time = None + else: + raise ValueError + + @property + def view_bound_scale(self): + return self._view_bound_scale + + @view_bound_scale.setter + def view_bound_scale(self, view_bound_scale): + if isinstance(view_bound_scale, basestring): + self._view_bound_scale = view_bound_scale + elif view_bound_scale is None: + self._view_bound_scale = None + else: + raise ValueError + + @property + def view_format(self): + return self._view_format + + @view_format.setter + def view_format(self, view_format): + if isinstance(view_format, basestring): + self._view_format = view_format + elif view_format is None: + self._view_format = None + else: + raise ValueError + + @property + def http_query(self): + return self._http_query + + @http_query.setter + def http_query(self, http_query): + if isinstance(http_query, basestring): + self._http_query = http_query + elif http_query is None: + self._http_query = None + else: + raise ValueError + + def etree_element(self): + element = super(Icon, self).etree_element() + + if self._href: + href = etree.SubElement(element, "%shref" % self.ns) + href.text = self._href + if self._refresh_mode: + refresh_mode = etree.SubElement(element, "%srefreshMode" % self.ns) + refresh_mode.text = self._refresh_mode + if self._refresh_interval: + refresh_interval = etree.SubElement(element, "%srefreshInterval" % self.ns) + refresh_interval.text = self._refresh_interval + if self._view_refresh_mode: + view_refresh_mode = etree.SubElement(element, "%sviewRefreshMode" % self.ns) + view_refresh_mode.text = self._view_refresh_mode + if self._view_refresh_time: + view_refresh_time = etree.SubElement(element, "%sviewRefreshTime" % self.ns) + view_refresh_time.text = self._view_refresh_time + if self._view_bound_scale: + view_bound_scale = etree.SubElement(element, "%sviewBoundScale" % self.ns) + view_bound_scale.text = self._view_bound_scale + if self._view_format: + view_format = etree.SubElement(element, "%sviewFormat" % self.ns) + view_format.text = self._view_format + if self._http_query: + http_query = etree.SubElement(element, "%shttpQuery" % self.ns) + http_query.text = self._http_query + + return element + + def from_element(self, element): + super(Icon, self).from_element(element) + + href = element.find('%shref' % self.ns) + if href is not None: + self.href = href.text + + refresh_mode = element.find('%srefreshMode' % self.ns) + if refresh_mode is not None: + self.refresh_mode = refresh_mode.text + + refresh_interval = element.find('%srefreshInterval' % self.ns) + if refresh_interval is not None: + self.refresh_interval = refresh_interval.text + + view_refresh_mode = element.find('%sviewRefreshMode' % self.ns) + if view_refresh_mode is not None: + self.view_refresh_mode = view_refresh_mode.text + + view_refresh_time = element.find('%sviewRefreshTime' % self.ns) + if view_refresh_time is not None: + self.view_refresh_time = view_refresh_time.text + + view_bound_scale = element.find('%sviewBoundScale' % self.ns) + if view_bound_scale is not None: + self.view_bound_scale = view_bound_scale.text + + view_format = element.find('%sviewFormat' % self.ns) + if view_format is not None: + self.view_format = view_format.text + + http_query = element.find('%shttpQuery' % self.ns) + if http_query is not None: + self.http_query = http_query.text From 305a940a82e9066e3c59bde30aac7855fbf42054 Mon Sep 17 00:00:00 2001 From: Kyle Linden Date: Wed, 1 Nov 2017 12:15:56 -0400 Subject: [PATCH 003/144] fix casing on element --- fastkml/kml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index d7f87aff..3ba83651 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -915,7 +915,7 @@ def etree_element(self): ) altitudeMode.text = self._altitudeMode if all([self._north, self._south, self._east, self._west]): - latLonBox = etree.SubElement(element, '%slatLonBox' % self.ns) + latLonBox = etree.SubElement(element, '%sLatLonBox' % self.ns) north = etree.SubElement(latLonBox, '%snorth' % self.ns) north.text = self._north south = etree.SubElement(latLonBox, '%ssouth' % self.ns) @@ -938,7 +938,7 @@ def from_element(self, element): altitudeMode = element.find('%saltitudeMode' % self.ns) if altitudeMode is not None: self.altitudeMode = altitudeMode.text - latLonBox = element.find('%slatLonBox' % self.ns) + latLonBox = element.find('%sLatLonBox' % self.ns) if latLonBox is not None: north = latLonBox.find('%snorth' % self.ns) if north is not None: From 699c87a7f0a9179d1f63d5750ca7b28d5713ddcf Mon Sep 17 00:00:00 2001 From: Kyle Linden Date: Fri, 13 Apr 2018 09:48:45 -0400 Subject: [PATCH 004/144] pypi package info --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 6dab726c..429731e7 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,9 @@ def run_tests(self): setup( - name='fastkml', + name='fastkml-klinden', version='0.11', - description="Fast KML processing in python", + description="Fast KML processing in python (forked from https://github.com/cleder/fastkml)", long_description=( open("README.rst").read() + "\n" + open(os.path.join("docs", "HISTORY.txt")).read() @@ -41,9 +41,9 @@ def run_tests(self): 'Operating System :: OS Independent', ], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='GIS KML Google Maps OpenLayers', - author='Christian Ledermann', - author_email='christian.ledermann@gmail.com', - url='https://github.com/cleder/fastkml', + author='Kyle Linden', + author_email='linden.kyle@gmail.com', + url='https://github.com/klinden/fastkml', license='LGPL', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, From 45660a0c43e8893223a68daff98c52cd7dcc4939 Mon Sep 17 00:00:00 2001 From: Andrew McDonnell Date: Thu, 28 Jun 2018 21:14:03 +0930 Subject: [PATCH 005/144] Parse KML files with GroudOverlay at the top --- fastkml/kml.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 22321e82..64a532a1 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -105,6 +105,11 @@ def from_string(self, xml_string): feature = Folder(ns) feature.from_element(folder) self.append(feature) + groundoverlays = element.findall('%sGroundOverlay' % ns) + for groundoverlay in groundoverlays: + feature = GroundOverlay(ns) + feature.from_element(groundoverlay) + self.append(feature) placemarks = element.findall('%sPlacemark' % ns) for placemark in placemarks: feature = Placemark(ns) @@ -147,21 +152,21 @@ def to_string(self, prettyprint=False): def features(self): """ iterate over features """ for feature in self._features: - if isinstance(feature, (Document, Folder, Placemark)): + if isinstance(feature, (Document, Folder, Placemark, GroundOverlay)): yield feature else: raise TypeError( "Features must be instances of " - "(Document, Folder, Placemark)" + "(Document, Folder, Placemark, GroundOverlay)" ) def append(self, kmlobj): """ append a feature """ - if isinstance(kmlobj, (Document, Folder, Placemark)): + if isinstance(kmlobj, (Document, Folder, Placemark, GroundOverlay)): self._features.append(kmlobj) else: raise TypeError( - "Features must be instances of (Document, Folder, Placemark)") + "Features must be instances of (Document, Folder, Placemark, GroundOverlay)") class _Feature(_BaseObject): From 012d26827fd6b192b2e8e0e4d5c6913e6f16e7c9 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 23 Sep 2021 17:19:58 +0100 Subject: [PATCH 006/144] fix bug introduced resolving conflicts --- fastkml/kml.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index d1d10cbd..dcfadc44 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -91,12 +91,6 @@ def from_string(self, xml_string): ) else: element = etree.XML(xml_string) - - placemarks = element.findall('%sPlacemark' % ns) - for placemark in placemarks: - feature = Placemark(ns) - feature.from_element(placemark) - self.append(feature) if not element.tag.endswith("kml"): raise TypeError @@ -120,7 +114,7 @@ def from_string(self, xml_string): for groundoverlay in groundoverlays: feature = GroundOverlay(ns) feature.from_element(groundoverlay) - self.append(feature) + self.append(feature) def etree_element(self): # self.ns may be empty, which leads to unprefixed kml elements. @@ -170,7 +164,7 @@ def append(self, kmlobj): raise TypeError( "Features must be instances of (Document, Folder, Placemark, Overlay)") - + class _Feature(_BaseObject): """ abstract element; do not create From 5c6b0b573f77acba1970009ce1e55ad39005b5b8 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Oct 2021 20:07:40 +0100 Subject: [PATCH 007/144] Development Status Alpha, version 1.0.alpha.0 --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a1076277..c4487ca3 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def run_tests(self): setup( name="fastkml", - version="1.0.alpha.1", + version="1.0.alpha.0", description="Fast KML processing in python", long_description=( open("README.rst").read() @@ -41,7 +41,8 @@ def run_tests(self): "Programming Language :: Python :: 3.10", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Development Status :: 5 - Production/Stable", + # "Development Status :: 5 - Production/Stable", + "Development Status :: 3 - Alpha" "Operating System :: OS Independent", ], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords="GIS KML Google Maps OpenLayers", From be3d5823637672eaa0cd2733924e388a4a2ffdeb Mon Sep 17 00:00:00 2001 From: djrobin17 Date: Fri, 29 Oct 2021 02:44:32 +0530 Subject: [PATCH 008/144] Added pre-commit --- .pre-commit-config.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..4535a11c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: mixed-line-ending +- repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black \ No newline at end of file From 70b11be548256bf1afb05f7d7597e522d7c834be Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Oct 2021 21:16:47 +0000 Subject: [PATCH 009/144] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/LICENSE.GPL | 18 ++++++++-------- docs/usage_guide.rst | 6 +++--- examples/KML_Samples.kml | 44 ++++++++++++++++++++-------------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/LICENSE.GPL b/docs/LICENSE.GPL index aae716b3..21152a2d 100644 --- a/docs/LICENSE.GPL +++ b/docs/LICENSE.GPL @@ -55,7 +55,7 @@ modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - + Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a @@ -111,7 +111,7 @@ modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - + GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION @@ -158,7 +158,7 @@ Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - + 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 @@ -216,7 +216,7 @@ instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - + Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. @@ -267,7 +267,7 @@ Library will still fall under Section 6.) distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - + 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work @@ -329,7 +329,7 @@ restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - + 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined @@ -370,7 +370,7 @@ subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - + 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or @@ -422,7 +422,7 @@ conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - + 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is @@ -456,7 +456,7 @@ SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS - + How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest diff --git a/docs/usage_guide.rst b/docs/usage_guide.rst index caa9de21..d21ed0e2 100644 --- a/docs/usage_guide.rst +++ b/docs/usage_guide.rst @@ -102,13 +102,13 @@ You can create a KML object by reading a KML file as a string # Start by importing the kml module >>> from fastkml import kml - + #Read file into string and convert to UTF-8 (Python3 style) >>> with open(kml_file, 'rt', encoding="utf-8") as myfile: ... doc=myfile.read() - + # OR - + # Setup the string which contains the KML file we want to read >>> doc = """ ... diff --git a/examples/KML_Samples.kml b/examples/KML_Samples.kml index 47ae59a4..3bb729f3 100644 --- a/examples/KML_Samples.kml +++ b/examples/KML_Samples.kml @@ -229,36 +229,36 @@ Placemark descriptions can be enriched by using many standard HTML tags.
For example:
Styles:
-Italics, -Bold, -Underlined, -Strike Out, -subscriptsubscript, -superscriptsuperscript, -Big, -Small, -Typewriter, -Emphasized, -Strong, +Italics, +Bold, +Underlined, +Strike Out, +subscriptsubscript, +superscriptsuperscript, +Big, +Small, +Typewriter, +Emphasized, +Strong, Code
-Fonts:
-red by name, +Fonts:
+red by name, leaf green by hexadecimal RGB
-size 1, -size 2, -size 3, -size 4, -size 5, -size 6, +size 1, +size 2, +size 3, +size 4, +size 5, +size 6, size 7
-Times, -Verdana, +Times, +Verdana, Arial

-Links: +Links:
Google Earth!
From 95a87565f42a0d7c384fa9714a2263d42e1d18cf Mon Sep 17 00:00:00 2001 From: djrobin17 Date: Fri, 29 Oct 2021 20:52:55 +0530 Subject: [PATCH 010/144] done requested changes --- .pre-commit-config.yaml | 10 +++++++++- test-requirements.txt | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4535a11c..49e56afc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,4 +7,12 @@ repos: - repo: https://github.com/psf/black rev: 20.8b1 hooks: - - id: black \ No newline at end of file + - id: black +- repo: https://gitlab.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 +- repo: https://github.com/pycqa/isort + rev: 5.8.0 + hooks: + - id: isort \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt index aa59444b..30dc6cbc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,3 +12,4 @@ isort radon lizard yamllint +pre-commit From a8c4de7f7807bcf8be54fea55b957493796970ab Mon Sep 17 00:00:00 2001 From: Daniel Lim Date: Sat, 30 Oct 2021 02:27:15 -0700 Subject: [PATCH 011/144] Add Dependabot configuration --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a9953cb6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +# Set update schedule for GitHub Actions + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" From 4333e5541ffb753b5c44b7abbe220706dd3629c9 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 30 Oct 2021 14:06:15 +0100 Subject: [PATCH 012/144] lint yaml --- .github/dependabot.yml | 3 ++- .pep8speaks.yml | 2 ++ .pre-commit-config.yaml | 20 +++++++++++--------- .pyup.yml | 10 ++++++---- docs/contributing.rst | 12 ++++++++++++ 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a9953cb6..02a95511 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,5 @@ # Set update schedule for GitHub Actions - +--- version: 2 updates: - package-ecosystem: "github-actions" @@ -7,3 +7,4 @@ updates: schedule: # Check for updates to GitHub Actions every weekday interval: "daily" +... diff --git a/.pep8speaks.yml b/.pep8speaks.yml index 4ba8d275..5b0ea0c8 100644 --- a/.pep8speaks.yml +++ b/.pep8speaks.yml @@ -1,5 +1,7 @@ +--- scanner: linter: flake8 flake8: max-line-length: 89 +... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49e56afc..373660dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,20 @@ +--- repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - - id: trailing-whitespace - - id: mixed-line-ending -- repo: https://github.com/psf/black + - id: trailing-whitespace + - id: mixed-line-ending + - repo: https://github.com/psf/black rev: 20.8b1 hooks: - - id: black -- repo: https://gitlab.com/pycqa/flake8 + - id: black + - repo: https://gitlab.com/pycqa/flake8 rev: 4.0.1 hooks: - - id: flake8 -- repo: https://github.com/pycqa/isort + - id: flake8 + - repo: https://github.com/pycqa/isort rev: 5.8.0 hooks: - - id: isort \ No newline at end of file + - id: isort +... diff --git a/.pyup.yml b/.pyup.yml index b2eaf00a..b6b6e03d 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -1,3 +1,4 @@ +--- # configure updates globally # default: all # allowed: all, insecure, False @@ -6,7 +7,7 @@ update: all # configure dependency pinning globally # default: True # allowed: True, False -pin: False +pin: false # set the default branch # default: empty, the default branch on GitHub @@ -20,15 +21,16 @@ schedule: "every day" # search for requirement files # default: True # allowed: True, False -search: True +search: true # Specify requirement files by hand, default is empty # default: empty # allowed: list requirements: - test-requirements.txt: - pin: False + pin: false # allow to close stale PRs # default: True -close_prs: True +close_prs: true +... diff --git a/docs/contributing.rst b/docs/contributing.rst index 722834f4..a2ee904e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -63,3 +63,15 @@ signifying that the code is working as expected on all configurations available. .. _tox: https://pypi.python.org/pypi/tox + +pre-commit +~~~~~~~~~~~ + +Install the ``pre-commit`` hook with:: + + pip install pre-commit + pre-commit install + +and check the code with:: + + pre-commit run --all-files From b3e4fbf3e4525b67d63c752c18716c5af6864d07 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 30 Oct 2021 14:13:16 +0100 Subject: [PATCH 013/144] improve pre-commit config --- .pre-commit-config.yaml | 25 ++++++++++++++++++++++++- docs/LICENSE.GPL | 2 -- docs/usage_guide.rst | 2 +- test-requirements.txt | 10 +++++----- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 373660dd..a279d8d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,8 +3,23 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - - id: trailing-whitespace + - id: check-added-large-files + - id: check-docstring-first + - id: check-json + - id: check-merge-conflict + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer - id: mixed-line-ending + - id: name-tests-test + args: ['--django'] + - id: no-commit-to-branch + - id: pretty-format-json + - id: requirements-txt-fixer + - id: trailing-whitespace - repo: https://github.com/psf/black rev: 20.8b1 hooks: @@ -17,4 +32,12 @@ repos: rev: 5.8.0 hooks: - id: isort + # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup + # rev: v1.0.1 + # hooks: + # - id: rst-linter + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v0.910 + # hooks: + # - id: mypy ... diff --git a/docs/LICENSE.GPL b/docs/LICENSE.GPL index 21152a2d..232af85a 100644 --- a/docs/LICENSE.GPL +++ b/docs/LICENSE.GPL @@ -500,5 +500,3 @@ necessary. Here is a sample; alter the names: Ty Coon, President of Vice That's all there is to it! - - diff --git a/docs/usage_guide.rst b/docs/usage_guide.rst index d21ed0e2..a1203ad2 100644 --- a/docs/usage_guide.rst +++ b/docs/usage_guide.rst @@ -94,7 +94,7 @@ Example how to build a simple KML file from the Python interpreter. Read a KML File/String ---------------- +---------------------- You can create a KML object by reading a KML file as a string diff --git a/test-requirements.txt b/test-requirements.txt index 30dc6cbc..4f9597bc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,15 +1,15 @@ -r requirements.txt -r doc-requirements.txt -pytest -pytest-cov +black flake8 flake8-dunder-all flake8-isort flake8-logging-format flake8-use-fstring -black isort -radon lizard -yamllint pre-commit +pytest +pytest-cov +radon +yamllint From b276a4ce16cc3efbe29dfe9f8dff07af786a3c8a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 30 Oct 2021 14:21:26 +0100 Subject: [PATCH 014/144] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f811d2a3..f207610b 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def run_tests(self): setup( name="fastkml", - version="1.0.alpha.1", + version="1.0.alpha.2", description="Fast KML processing in python", long_description=( open("README.rst").read() From 05a6884783c7edacc2a3d3f29f0901cfeee9dabd Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 30 Oct 2021 14:47:54 +0100 Subject: [PATCH 015/144] fix pep8 names --- fastkml/geometry.py | 4 ++-- fastkml/styles.py | 12 ++++++------ fastkml/test_main.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 91ad193d..4f6cf00b 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -17,7 +17,7 @@ import logging import re -from pygeoif.factories import shape as asShape +from pygeoif.factories import shape from pygeoif.geometry import GeometryCollection from pygeoif.geometry import LinearRing from pygeoif.geometry import LineString @@ -117,7 +117,7 @@ def __init__( ): self.geometry = geometry else: - self.geometry = asShape(geometry) + self.geometry = shape(geometry) # write kml diff --git a/fastkml/styles.py b/fastkml/styles.py index b8863e20..50f6f1ff 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -213,23 +213,23 @@ class _ColorStyle(_BaseObject): # The order of expression is aabbggrr, where aa=alpha (00 to ff); # bb=blue (00 to ff); gg=green (00 to ff); rr=red (00 to ff). - colorMode = None + color_mode = None # Values for are normal (no effect) and random. # A value of random applies a random linear scale to the base - def __init__(self, ns=None, id=None, color=None, colorMode=None): + def __init__(self, ns=None, id=None, color=None, color_mode=None): super().__init__(ns, id) self.color = color - self.colorMode = colorMode + self.color_mode = color_mode def etree_element(self): element = super().etree_element() if self.color: color = etree.SubElement(element, f"{self.ns}color") color.text = self.color - if self.colorMode: + if self.color_mode: colorMode = etree.SubElement(element, f"{self.ns}colorMode") - colorMode.text = self.colorMode + colorMode.text = self.color_mode return element def from_element(self, element): @@ -237,7 +237,7 @@ def from_element(self, element): super().from_element(element) colorMode = element.find(f"{self.ns}colorMode") if colorMode is not None: - self.colorMode = colorMode.text + self.color_mode = colorMode.text color = element.find(f"{self.ns}color") if color is not None: self.color = color.text diff --git a/fastkml/test_main.py b/fastkml/test_main.py index aec7c38d..54b98250 100644 --- a/fastkml/test_main.py +++ b/fastkml/test_main.py @@ -1138,7 +1138,7 @@ def test_labelstyle(self): style = list(list(list(k.features())[0].styles())[0].styles())[0] self.assertIsInstance(style, styles.LabelStyle) self.assertEqual(style.color, "ff0000cc") - self.assertEqual(style.colorMode, None) + self.assertEqual(style.color_mode, None) k2 = kml.KML() k2.from_string(k.to_string()) self.assertEqual(k.to_string(), k2.to_string()) @@ -1168,7 +1168,7 @@ def test_iconstyle(self): self.assertIsInstance(style, styles.IconStyle) self.assertEqual(style.color, "ff00ff00") self.assertEqual(style.scale, 1.1) - self.assertEqual(style.colorMode, "random") + self.assertEqual(style.color_mode, "random") self.assertEqual(style.heading, 0.0) self.assertEqual(style.icon_href, "http://maps.google.com/icon21.png") k2 = kml.KML() @@ -1223,7 +1223,7 @@ def test_polystyle(self): style = list(list(list(k.features())[0].styles())[0].styles())[0] self.assertIsInstance(style, styles.PolyStyle) self.assertEqual(style.color, "ff0000cc") - self.assertEqual(style.colorMode, "random") + self.assertEqual(style.color_mode, "random") k2 = kml.KML() k2.from_string(k.to_string()) self.assertEqual(k.to_string(), k2.to_string()) From f3de4bdf559b31913145e002915f713e7e39a14c Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 30 Oct 2021 15:05:50 +0100 Subject: [PATCH 016/144] fix pep8 names --- fastkml/base.py | 8 ++--- fastkml/styles.py | 82 +++++++++++++++++++++++--------------------- fastkml/test_main.py | 14 ++++---- 3 files changed, 53 insertions(+), 51 deletions(-) diff --git a/fastkml/base.py b/fastkml/base.py index 28a70d94..f904f22e 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -66,7 +66,7 @@ class _BaseObject(_XMLObject): mechanism is to be used.""" id = None - targetId = None + target_id = None def __init__(self, ns=None, id=None): super().__init__(ns) @@ -77,8 +77,8 @@ def etree_element(self): element = super().etree_element() if self.id: element.set("id", self.id) - if self.targetId: - element.set("targetId", self.targetId) + if self.target_id: + element.set("targetId", self.target_id) return element def from_element(self, element): @@ -86,4 +86,4 @@ def from_element(self, element): if element.get("id"): self.id = element.get("id") if element.get("targetId"): - self.targetId = element.get("targetId") + self.target_id = element.get("targetId") diff --git a/fastkml/styles.py b/fastkml/styles.py index 50f6f1ff..d42dc5e6 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -228,16 +228,16 @@ def etree_element(self): color = etree.SubElement(element, f"{self.ns}color") color.text = self.color if self.color_mode: - colorMode = etree.SubElement(element, f"{self.ns}colorMode") - colorMode.text = self.color_mode + color_mode = etree.SubElement(element, f"{self.ns}colorMode") + color_mode.text = self.color_mode return element def from_element(self, element): super().from_element(element) - colorMode = element.find(f"{self.ns}colorMode") - if colorMode is not None: - self.color_mode = colorMode.text + color_mode = element.find(f"{self.ns}colorMode") + if color_mode is not None: + self.color_mode = color_mode.text color = element.find(f"{self.ns}color") if color is not None: self.color = color.text @@ -260,12 +260,12 @@ def __init__( ns=None, id=None, color=None, - colorMode=None, + color_mode=None, scale=1.0, heading=None, icon_href=None, ): - super().__init__(ns, id, color, colorMode) + super().__init__(ns, id, color, color_mode) self.scale = scale self.heading = heading self.icon_href = icon_href @@ -311,8 +311,8 @@ class LineStyle(_ColorStyle): width = 1.0 # Width of the line, in pixels. - def __init__(self, ns=None, id=None, color=None, colorMode=None, width=1): - super().__init__(ns, id, color, colorMode) + def __init__(self, ns=None, id=None, color=None, color_mode=None, width=1): + super().__init__(ns, id, color, color_mode) self.width = width def etree_element(self): @@ -343,8 +343,10 @@ class PolyStyle(_ColorStyle): # Boolean value. Specifies whether to outline the polygon. # Polygon outlines use the current LineStyle. - def __init__(self, ns=None, id=None, color=None, colorMode=None, fill=1, outline=1): - super().__init__(ns, id, color, colorMode) + def __init__( + self, ns=None, id=None, color=None, color_mode=None, fill=1, outline=1 + ): + super().__init__(ns, id, color, color_mode) self.fill = fill self.outline = outline @@ -377,8 +379,8 @@ class LabelStyle(_ColorStyle): scale = 1.0 # Resizes the label. - def __init__(self, ns=None, id=None, color=None, colorMode=None, scale=1.0): - super().__init__(ns, id, color, colorMode) + def __init__(self, ns=None, id=None, color=None, color_mode=None, scale=1.0): + super().__init__(ns, id, color, color_mode) self.scale = scale def etree_element(self): @@ -402,7 +404,7 @@ class BalloonStyle(_BaseObject): __name__ = "BalloonStyle" - bgColor = None + bg_color = None # Background color of the balloon (optional). Color and opacity (alpha) # values are expressed in hexadecimal notation. The range of values for # any one color is 0 to 255 (00 to ff). The order of expression is @@ -416,7 +418,7 @@ class BalloonStyle(_BaseObject): # Note: The use of the element within has been # deprecated. Use instead. - textColor = None + text_color = None # Foreground color for text. The default is black (ff000000). text = None @@ -439,7 +441,7 @@ class BalloonStyle(_BaseObject): # in the Feature elements that use this BalloonStyle: # This is $[name], whose description is:
$[description]
- displayMode = None + display_mode = None # If is default, Google Earth uses the information supplied # in to create a balloon . If is hide, Google Earth # does not display the balloon. In Google Earth, clicking the List View @@ -450,50 +452,50 @@ def __init__( self, ns=None, id=None, - bgColor=None, - textColor=None, + bg_color=None, + text_color=None, text=None, - displayMode=None, + display_mode=None, ): super().__init__(ns, id) - self.bgColor = bgColor - self.textColor = textColor + self.bg_color = bg_color + self.text_color = text_color self.text = text - self.displayMode = displayMode + self.display_mode = display_mode def from_element(self, element): super().from_element(element) - bgColor = element.find(f"{self.ns}bgColor") - if bgColor is not None: - self.bgColor = bgColor.text + bg_color = element.find(f"{self.ns}bgColor") + if bg_color is not None: + self.bg_color = bg_color.text else: - bgColor = element.find(f"{self.ns}color") - if bgColor is not None: - self.bgColor = bgColor.text - textColor = element.find(f"{self.ns}textColor") - if textColor is not None: - self.textColor = textColor.text + bg_color = element.find(f"{self.ns}color") + if bg_color is not None: + self.bg_color = bg_color.text + text_color = element.find(f"{self.ns}textColor") + if text_color is not None: + self.text_color = text_color.text text = element.find(f"{self.ns}text") if text is not None: self.text = text.text - displayMode = element.find(f"{self.ns}displayMode") - if displayMode is not None: - self.displayMode = displayMode.text + display_mode = element.find(f"{self.ns}displayMode") + if display_mode is not None: + self.display_mode = display_mode.text def etree_element(self): element = super().etree_element() - if self.bgColor is not None: + if self.bg_color is not None: elem = etree.SubElement(element, f"{self.ns}bgColor") - elem.text = self.bgColor - if self.textColor is not None: + elem.text = self.bg_color + if self.text_color is not None: elem = etree.SubElement(element, f"{self.ns}textColor") - elem.text = self.textColor + elem.text = self.text_color if self.text is not None: elem = etree.SubElement(element, f"{self.ns}text") elem.text = self.text - if self.displayMode is not None: + if self.display_mode is not None: elem = etree.SubElement(element, f"{self.ns}displayMode") - elem.text = self.displayMode + elem.text = self.display_mode return element diff --git a/fastkml/test_main.py b/fastkml/test_main.py index 54b98250..800d857d 100644 --- a/fastkml/test_main.py +++ b/fastkml/test_main.py @@ -45,10 +45,10 @@ def test_base_object(self): bo = base._BaseObject(id="id0") self.assertEqual(bo.id, "id0") self.assertEqual(bo.ns, config.KMLNS) - self.assertEqual(bo.targetId, None) + self.assertEqual(bo.target_id, None) self.assertEqual(bo.__name__, None) - bo.targetId = "target" - self.assertEqual(bo.targetId, "target") + bo.target_id = "target" + self.assertEqual(bo.target_id, "target") bo.ns = "" bo.id = None self.assertEqual(bo.id, None) @@ -1085,9 +1085,9 @@ def test_balloonstyle(self): self.assertIsInstance(list(list(k.features())[0].styles())[0], styles.Style) style = list(list(list(k.features())[0].styles())[0].styles())[0] self.assertIsInstance(style, styles.BalloonStyle) - self.assertEqual(style.bgColor, "ffffffbb") - self.assertEqual(style.textColor, "ff000000") - self.assertEqual(style.displayMode, "default") + self.assertEqual(style.bg_color, "ffffffbb") + self.assertEqual(style.text_color, "ff000000") + self.assertEqual(style.display_mode, "default") self.assertIn("$[geDirections]", style.text) self.assertIn("$[description]", style.text) k2 = kml.KML() @@ -1113,7 +1113,7 @@ def test_balloonstyle_old_color(self): self.assertIsInstance(list(list(k.features())[0].styles())[0], styles.Style) style = list(list(list(k.features())[0].styles())[0].styles())[0] self.assertIsInstance(style, styles.BalloonStyle) - self.assertEqual(style.bgColor, "ffffffbb") + self.assertEqual(style.bg_color, "ffffffbb") k2 = kml.KML() k2.from_string(k.to_string()) self.assertEqual(k2.to_string(), k.to_string()) From 3c30cb817cdb0c8dbdf85a72a966cb6eab442d9b Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 30 Oct 2021 16:08:40 +0100 Subject: [PATCH 017/144] add pypy 3.8 to tests --- .github/workflows/run-all-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index cf16a65f..f6ebb85d 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - pypy-version: ['pypy-3.6', 'pypy-3.7'] + pypy-version: ['pypy-3.6', 'pypy-3.7', 'pypy-3.8'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.pypy-version }} From d08060cf7b10d0a9224e51760e9f4920cf6368b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cteerasak=E2=80=9D?= Date: Sat, 30 Oct 2021 23:14:46 +0700 Subject: [PATCH 018/144] fix PEP8 naming --- fastkml/kml.py | 168 +++++++++++++++++++++---------------------- fastkml/test_main.py | 84 +++++++++++----------- 2 files changed, 126 insertions(+), 126 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 4621b220..cc7302ea 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -183,7 +183,7 @@ class _Feature(_BaseObject): # You can use the
tag to specify the location of a point # instead of using latitude and longitude coordinates. - _phoneNumber = None + _phone_number = None # A string value representing a telephone number. # This element is used by Google Maps Mobile only. @@ -204,7 +204,7 @@ class _Feature(_BaseObject): description = None # User-supplied content that appears in the description balloon. - _styleUrl = None + _style_url = None # URL of a + + """ + + k = kml.KML() + k.from_string(doc) + style = list(list(list(k.features())[0].styles())[0].styles())[0] + self.assertIsInstance(style, styles.PolyStyle) + self.assertEqual(style.fill, 0) + k2 = kml.KML() + k2.from_string(k.to_string()) + self.assertEqual(k.to_string(), k2.to_string()) + + def test_polystyle_boolean_outline(self): + doc = """ + + PolygonStyle.kml + 1 + + + """ + + k = kml.KML() + k.from_string(doc) + style = list(list(list(k.features())[0].styles())[0].styles())[0] + self.assertIsInstance(style, styles.PolyStyle) + self.assertEqual(style.outline, 0) + k2 = kml.KML() + k2.from_string(k.to_string()) + self.assertEqual(k.to_string(), k2.to_string()) + def test_polystyle_float_fill(self): doc = """ From 40c18ea450da5a26271c250e4a8307e81e6024dc Mon Sep 17 00:00:00 2001 From: ivanlonel Date: Sun, 24 Apr 2022 13:02:14 -0300 Subject: [PATCH 110/144] Module distutils is deprecated --- fastkml/styles.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/fastkml/styles.py b/fastkml/styles.py index 9358fac3..f936aa54 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -20,7 +20,6 @@ part of how your data is displayed. """ -import distutils import logging from fastkml.base import _BaseObject @@ -362,19 +361,21 @@ def etree_element(self): return element def from_element(self, element): + def strtobool (val): + val = val.lower() + if val == 'false': + return 0 + if val == 'true': + return 1 + return int(float(val)) + super().from_element(element) fill = element.find(f"{self.ns}fill") if fill is not None: - try: - self.fill = distutils.util.strtobool(fill.text) - except ValueError: - self.fill = int(float(fill.text)) - outline = element.find("%soutline" % self.ns) + self.fill = strtobool(fill.text) + outline = element.find(f"{self.ns}outline") if outline is not None: - try: - self.outline = distutils.util.strtobool(outline.text) - except ValueError: - self.outline = int(float(outline.text)) + self.outline = strtobool(outline.text) class LabelStyle(_ColorStyle): From 0e4c8dc5ebb96ba690eb8e64ac7ceb43c2ad85b7 Mon Sep 17 00:00:00 2001 From: ivanlonel Date: Sun, 24 Apr 2022 13:41:38 -0300 Subject: [PATCH 111/144] PEP 8 compliance: whitespace before '(' --- fastkml/styles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastkml/styles.py b/fastkml/styles.py index f936aa54..2dc88e82 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -361,7 +361,7 @@ def etree_element(self): return element def from_element(self, element): - def strtobool (val): + def strtobool(val): val = val.lower() if val == 'false': return 0 From edd7728357d1adeca39a0b683055c3cda8b29138 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 2 Jun 2022 10:38:22 +0100 Subject: [PATCH 112/144] Create codeball.yml --- .github/workflows/codeball.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/codeball.yml diff --git a/.github/workflows/codeball.yml b/.github/workflows/codeball.yml new file mode 100644 index 00000000..ea0bf4a8 --- /dev/null +++ b/.github/workflows/codeball.yml @@ -0,0 +1,12 @@ +name: Codeball +on: [pull_request] + +jobs: + codeball_job: + runs-on: ubuntu-latest + name: Codeball + steps: + # Run Codeball on all new Pull Requests 🚀 + # For customizations and more documentation, see https://github.com/sturdy-dev/codeball-action + - name: Codeball + uses: sturdy-dev/codeball-action@v2 From f8e63ec07e49b9a34c02a6f9bc7510c29b8142ca Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 5 Oct 2022 18:24:26 +0100 Subject: [PATCH 113/144] drop python 3.6 support --- .github/workflows/run-all-tests.yml | 6 +++--- .pre-commit-config.yaml | 10 +++++----- fastkml/styles.py | 4 ++-- setup.py | 5 +++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index f6ebb85d..bf4551c1 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev'] steps: - uses: actions/checkout@v2 @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev'] steps: - uses: actions/checkout@v2 @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - pypy-version: ['pypy-3.6', 'pypy-3.7', 'pypy-3.8'] + pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.pypy-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc0ccf6c..e55e19be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v4.3.0 hooks: - id: check-added-large-files - id: check-docstring-first @@ -21,19 +21,19 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.8.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 - rev: 4.0.1 + rev: 3.9.2 hooks: - id: flake8 - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/mgedmin/check-manifest - rev: "0.46" + rev: "0.48" hooks: - id: check-manifest # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup diff --git a/fastkml/styles.py b/fastkml/styles.py index 2dc88e82..3311cfbd 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -363,9 +363,9 @@ def etree_element(self): def from_element(self, element): def strtobool(val): val = val.lower() - if val == 'false': + if val == "false": return 0 - if val == 'true': + if val == "true": return 1 return int(float(val)) diff --git a/setup.py b/setup.py index f207610b..82252710 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,6 @@ def run_tests(self): "Topic :: Scientific/Engineering :: GIS", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -55,10 +54,12 @@ def run_tests(self): zip_safe=False, tests_require=["pytest"], cmdclass={"test": PyTest}, + python_requires=">=3.7", install_requires=[ # -*- Extra requirements: -*- - "pygeoif>=1.0b1", + "pygeoif>=1.0.0", "python-dateutil", + "typing_extensions", ], entry_points=""" # -*- Entry points: -*- From e836684b30965efbde0dab088cdb07d24e2ddcaa Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 6 Oct 2022 10:02:04 +0100 Subject: [PATCH 114/144] resolve merge conflicts --- fastkml/kml.py | 4 ++-- fastkml/tests/oldunit_test.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index acdfaf39..4d8d24e2 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -313,8 +313,8 @@ def time_stamp(self): @time_stamp.setter def time_stamp(self, dt): - self._time_stamp = None if dt is None else TimeStamp(timestamp=dt) - if self._time_span is not None: + self._timestamp = None if dt is None else TimeStamp(timestamp=dt) + if self._timespan is not None: logger.warning("Setting a TimeStamp, TimeSpan deleted") self._timespan = None diff --git a/fastkml/tests/oldunit_test.py b/fastkml/tests/oldunit_test.py index 1d458ddd..002786d0 100644 --- a/fastkml/tests/oldunit_test.py +++ b/fastkml/tests/oldunit_test.py @@ -94,8 +94,8 @@ def test_feature(self): assert f.description is None assert f._style_url is None assert f._styles == [] - assert f._time_span is None - assert f._time_stamp is None + assert f._timespan is None + assert f._timestamp is None # self.assertEqual(f.region, None) # self.assertEqual(f.extended_data, None) From 5828c7c3923febabe448c701cad09ca41ebf3850 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:18:08 +0000 Subject: [PATCH 115/144] Bump codecov/codecov-action from 2 to 3 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2 to 3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v2...v3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/run-all-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 40738b4e..b3177ca2 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -46,7 +46,7 @@ jobs: run: | pytest fastkml --cov=fastkml --cov-fail-under=97 --cov-report=xml - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: fail_ci_if_error: true From 42351127b608d479c4bb5e3d9894dc9c05ae5f26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:18:13 +0000 Subject: [PATCH 116/144] Bump actions/checkout from 2 to 3.1.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3.1.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codesee-arch-diagram.yml | 2 +- .github/workflows/run-all-tests.yml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml index 5ff5de7b..ce138c70 100644 --- a/.github/workflows/codesee-arch-diagram.yml +++ b/.github/workflows/codesee-arch-diagram.yml @@ -15,7 +15,7 @@ jobs: steps: - name: checkout id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3.1.0 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 40738b4e..d380d0c0 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -12,7 +12,7 @@ jobs: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -32,7 +32,7 @@ jobs: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -57,7 +57,7 @@ jobs: python-version: ['3.9'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -86,7 +86,7 @@ jobs: matrix: pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.pypy-version }} uses: actions/setup-python@v2 with: @@ -104,7 +104,7 @@ jobs: name: Build and publish to PyPI and TestPyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v3.1.0 - name: Set up Python 3.9 uses: actions/setup-python@v1 with: From 6c5efa7201b8f11114258d367f50afaf1af88d16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Oct 2022 10:14:04 +0000 Subject: [PATCH 117/144] Bump actions/setup-python from 2 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codesee-arch-diagram.yml | 2 +- .github/workflows/run-all-tests.yml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml index 5ff5de7b..a01788af 100644 --- a/.github/workflows/codesee-arch-diagram.yml +++ b/.github/workflows/codesee-arch-diagram.yml @@ -42,7 +42,7 @@ jobs: node-version: '14' - name: Configure Python 3.x - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 if: ${{ fromJSON(steps.detect-languages.outputs.languages).python }} with: python-version: '3.10' diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index b3177ca2..600f6f71 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -59,7 +59,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.pypy-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.pypy-version }} - name: Install dependencies @@ -106,7 +106,7 @@ jobs: steps: - uses: actions/checkout@master - name: Set up Python 3.9 - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.9 - name: Install pypa/build From 20d4109229c4f4e0e70a206e04273270d869d2e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Oct 2022 10:28:55 +0000 Subject: [PATCH 118/144] Bump actions/setup-node from 2 to 3 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codesee-arch-diagram.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml index ce138c70..6508b525 100644 --- a/.github/workflows/codesee-arch-diagram.yml +++ b/.github/workflows/codesee-arch-diagram.yml @@ -36,7 +36,7 @@ jobs: # CodeSee Maps Go support uses a static binary so there's no setup step required. - name: Configure Node.js 14 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }} with: node-version: '14' From f77937ebd9de684b97c987c49f2d6ba875f5ee75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Oct 2022 11:25:20 +0000 Subject: [PATCH 119/144] Bump actions/setup-java from 2 to 3 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codesee-arch-diagram.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml index ea2aa7a4..7af0e1ca 100644 --- a/.github/workflows/codesee-arch-diagram.yml +++ b/.github/workflows/codesee-arch-diagram.yml @@ -27,7 +27,7 @@ jobs: uses: Codesee-io/codesee-detect-languages-action@latest - name: Configure JDK 16 - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 if: ${{ fromJSON(steps.detect-languages.outputs.languages).java }} with: java-version: '16' From 098e21ae0306b1c239fb9b0d623c14258d18ce64 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 6 Oct 2022 17:43:52 +0100 Subject: [PATCH 120/144] validate easing, etc to be between -180 and 180 --- fastkml/kml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 2f7698b0..887adeca 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -1261,11 +1261,11 @@ def lat_lon_box(self, north, south, east, west, rotation=0): self.east = east else: raise ValueError - if -180 <= float(east) <= 180: + if -180 <= float(west) <= 180: self.west = west else: raise ValueError - if -180 <= float(east) <= 180: + if -180 <= float(rotation) <= 180: self.rotation = rotation else: raise ValueError From 37e81f6a081cafec1059747239d5034de43f6b3e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 6 Oct 2022 18:12:46 +0100 Subject: [PATCH 121/144] pep8 --- fastkml/kml.py | 244 ++++++++++++++++++++++++---------------------- fastkml/styles.py | 38 +++++--- 2 files changed, 148 insertions(+), 134 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 4d8d24e2..e3ed0662 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -801,19 +801,19 @@ class PhotoOverlay(_Overlay): # the scene. A large field of view, like a wide-angle lens, focuses on a # large part of the scene. - _leftFov = None + _left_fow = None # Angle, in degrees, between the camera's viewing direction and the left side # of the view volume. - _rightFov = None + _right_fov = None # Angle, in degrees, between the camera's viewing direction and the right side # of the view volume. - _bottomFov = None + _bottom_fov = None # Angle, in degrees, between the camera's viewing direction and the bottom side # of the view volume. - _topFov = None + _top_fov = None # Angle, in degrees, between the camera's viewing direction and the top side # of the view volume. @@ -821,18 +821,18 @@ class PhotoOverlay(_Overlay): # Measurement in meters along the viewing direction from the camera viewpoint # to the PhotoOverlay shape. - _tileSize = "256" + _tile_size = "256" # Size of the tiles, in pixels. Tiles must be square, and must # be a power of 2. A tile size of 256 (the default) or 512 is recommended. # The original image is divided into tiles of this size, at varying resolutions. - _maxWidth = None + _max_width = None # Width in pixels of the original image. - _maxHeight = None + _max_height = None # Height in pixels of the original image. - _gridOrigin = None + _grid_origin = None # Specifies where to begin numbering the tiles in each layer of the pyramid. # A value of lowerLeft specifies that row 1, column 1 of each layer is in # the bottom left corner of the grid. @@ -867,54 +867,54 @@ def rotation(self, value): raise ValueError @property - def leftFov(self): - return self._leftFov + def left_fov(self): + return self._left_fow - @leftFov.setter - def leftFov(self, value): + @left_fov.setter + def left_fov(self, value): if isinstance(value, (str, int, float)): - self._leftFov = str(value) + self._left_fow = str(value) elif value is None: - self._leftFov = None + self._left_fow = None else: raise ValueError @property - def rightFov(self): - return self._rightFov + def right_fov(self): + return self._right_fov - @rightFov.setter - def rightFov(self, value): + @right_fov.setter + def right_fov(self, value): if isinstance(value, (str, int, float)): - self._rightFov = str(value) + self._right_fov = str(value) elif value is None: - self._rightFov = None + self._right_fov = None else: raise ValueError @property - def bottomFov(self): - return self._bottomFov + def bottom_fov(self): + return self._bottom_fov - @bottomFov.setter - def bottomFov(self, value): + @bottom_fov.setter + def bottom_fov(self, value): if isinstance(value, (str, int, float)): - self._bottomFov = str(value) + self._bottom_fov = str(value) elif value is None: - self._bottomFov = None + self._bottom_fov = None else: raise ValueError @property - def topFov(self): - return self._topFov + def top_fov(self): + return self._top_fov - @topFov.setter - def topFov(self, value): + @top_fov.setter + def top_fov(self, value): if isinstance(value, (str, int, float)): - self._topFov = str(value) + self._top_fov = str(value) elif value is None: - self._topFov = None + self._top_fov = None else: raise ValueError @@ -932,54 +932,54 @@ def near(self, value): raise ValueError @property - def tileSize(self): - return self._tileSize + def tile_size(self): + return self._tile_size - @tileSize.setter - def tileSize(self, value): + @tile_size.setter + def tile_size(self, value): if isinstance(value, (str, int, float)): - self._tileSize = str(value) + self._tile_size = str(value) elif value is None: - self._tileSize = None + self._tile_size = None else: raise ValueError @property - def maxWidth(self): - return self._maxWidth + def max_width(self): + return self._max_width - @maxWidth.setter - def maxWidth(self, value): + @max_width.setter + def max_width(self, value): if isinstance(value, (str, int, float)): - self._maxWidth = str(value) + self._max_width = str(value) elif value is None: - self._maxWidth = None + self._max_width = None else: raise ValueError @property - def maxHeight(self): - return self._maxHeight + def max_height(self): + return self._max_height - @maxHeight.setter - def maxHeight(self, value): + @max_height.setter + def max_height(self, value): if isinstance(value, (str, int, float)): - self._maxHeight = str(value) + self._max_height = str(value) elif value is None: - self._maxHeight = None + self._max_height = None else: raise ValueError @property - def gridOrigin(self): - return self._gridOrigin + def grid_origin(self): + return self._grid_origin - @gridOrigin.setter - def gridOrigin(self, value): + @grid_origin.setter + def grid_origin(self, value): if value in ("lowerLeft", "upperLeft"): - self._gridOrigin = str(value) + self._grid_origin = str(value) elif value is None: - self._gridOrigin = None + self._grid_origin = None else: raise ValueError @@ -1007,18 +1007,18 @@ def shape(self, value): else: raise ValueError("Shape must be one of " "rectangle, cylinder, sphere") - def ViewVolume(self, leftFov, rightFov, bottomFov, topFov, near): - self.leftFov = leftFov - self.rightFov = rightFov - self.bottomFov = bottomFov - self.topFov = topFov + def view_volume(self, left_fov, right_fov, bottom_fov, top_fov, near): + self.left_fov = left_fov + self.right_fov = right_fov + self.bottom_fov = bottom_fov + self.top_fov = top_fov self.near = near - def ImagePyramid(self, tileSize, maxWidth, maxHeight, gridOrigin): - self.tileSize = tileSize - self.maxWidth = maxWidth - self.maxHeight = maxHeight - self.gridOrigin = gridOrigin + def image_pyramid(self, tile_size, max_width, max_height, grid_origin): + self.tile_size = tile_size + self.max_width = max_width + self.max_height = max_height + self.grid_origin = grid_origin def etree_element(self): element = super().etree_element() @@ -1026,29 +1026,35 @@ def etree_element(self): rotation = config.etree.SubElement(element, f"{self.ns}rotation") rotation.text = self._rotation if all( - [self._leftFov, self._rightFov, self._bottomFov, self._topFov, self._near] + [ + self._left_fow, + self._right_fov, + self._bottom_fov, + self._top_fov, + self._near, + ] ): - ViewVolume = config.etree.SubElement(element, f"{self.ns}ViewVolume") - leftFov = config.etree.SubElement(ViewVolume, f"{self.ns}leftFov") - leftFov.text = self._leftFov - rightFov = config.etree.SubElement(ViewVolume, f"{self.ns}rightFov") - rightFov.text = self._rightFov - bottomFov = config.etree.SubElement(ViewVolume, f"{self.ns}bottomFov") - bottomFov.text = self._bottomFov - topFov = config.etree.SubElement(ViewVolume, f"{self.ns}topFov") - topFov.text = self._topFov - near = config.etree.SubElement(ViewVolume, f"{self.ns}near") + view_volume = config.etree.SubElement(element, f"{self.ns}ViewVolume") + left_fov = config.etree.SubElement(view_volume, f"{self.ns}leftFov") + left_fov.text = self._left_fow + right_fov = config.etree.SubElement(view_volume, f"{self.ns}rightFov") + right_fov.text = self._right_fov + bottom_fov = config.etree.SubElement(view_volume, f"{self.ns}bottomFov") + bottom_fov.text = self._bottom_fov + top_fov = config.etree.SubElement(view_volume, f"{self.ns}topFov") + top_fov.text = self._top_fov + near = config.etree.SubElement(view_volume, f"{self.ns}near") near.text = self._near - if all([self._tileSize, self._maxWidth, self._maxHeight, self._gridOrigin]): - ImagePyramid = config.etree.SubElement(element, f"{self.ns}ImagePyramid") - tileSize = config.etree.SubElement(ImagePyramid, f"{self.ns}tileSize") - tileSize.text = self._tileSize - maxWidth = config.etree.SubElement(ImagePyramid, f"{self.ns}maxWidth") - maxWidth.text = self._maxWidth - maxHeight = config.etree.SubElement(ImagePyramid, f"{self.ns}maxHeight") - maxHeight.text = self._maxHeight - gridOrigin = config.etree.SubElement(ImagePyramid, f"{self.ns}gridOrigin") - gridOrigin.text = self._gridOrigin + if all([self._tile_size, self._max_width, self._max_height, self._grid_origin]): + image_pyramid = config.etree.SubElement(element, f"{self.ns}ImagePyramid") + tile_size = config.etree.SubElement(image_pyramid, f"{self.ns}tileSize") + tile_size.text = self._tile_size + max_width = config.etree.SubElement(image_pyramid, f"{self.ns}maxWidth") + max_width.text = self._max_width + max_height = config.etree.SubElement(image_pyramid, f"{self.ns}maxHeight") + max_height.text = self._max_height + grid_origin = config.etree.SubElement(image_pyramid, f"{self.ns}gridOrigin") + grid_origin.text = self._grid_origin return element def from_element(self, element): @@ -1056,40 +1062,40 @@ def from_element(self, element): rotation = element.find(f"{self.ns}rotation") if rotation is not None: self.rotation = rotation.text - ViewVolume = element.find(f"{self.ns}ViewVolume") - if ViewVolume is not None: - leftFov = ViewVolume.find(f"{self.ns}leftFov") - if leftFov is not None: - self.leftFov = leftFov.text - rightFov = ViewVolume.find(f"{self.ns}rightFov") - if rightFov is not None: - self.rightFov = rightFov.text - bottomFov = ViewVolume.find(f"{self.ns}bottomFov") - if bottomFov is not None: - self.bottomFov = bottomFov.text - topFov = ViewVolume.find(f"{self.ns}topFov") - if topFov is not None: - self.topFov = topFov.text - near = ViewVolume.find(f"{self.ns}near") + view_volume = element.find(f"{self.ns}ViewVolume") + if view_volume is not None: + left_fov = view_volume.find(f"{self.ns}leftFov") + if left_fov is not None: + self.left_fov = left_fov.text + right_fov = view_volume.find(f"{self.ns}rightFov") + if right_fov is not None: + self.right_fov = right_fov.text + bottom_fov = view_volume.find(f"{self.ns}bottomFov") + if bottom_fov is not None: + self.bottom_fov = bottom_fov.text + top_fov = view_volume.find(f"{self.ns}topFov") + if top_fov is not None: + self.top_fov = top_fov.text + near = view_volume.find(f"{self.ns}near") if near is not None: self.near = near.text - ImagePyramid = element.find(f"{self.ns}ImagePyramid") - if ImagePyramid is not None: - tileSize = ImagePyramid.find(f"{self.ns}tileSize") - if tileSize is not None: - self.tileSize = tileSize.text - maxWidth = ImagePyramid.find(f"{self.ns}maxWidth") - if maxWidth is not None: - self.maxWidth = maxWidth.text - maxHeight = ImagePyramid.find(f"{self.ns}maxHeight") - if maxHeight is not None: - self.maxHeight = maxHeight.text - gridOrigin = ImagePyramid.find(f"{self.ns}gridOrigin") - if gridOrigin is not None: - self.gridOrigin = gridOrigin.text - Point = element.find(f"{self.ns}Point") - if Point is not None: - self.point = Point.text + image_pyramid = element.find(f"{self.ns}ImagePyramid") + if image_pyramid is not None: + tile_size = image_pyramid.find(f"{self.ns}tileSize") + if tile_size is not None: + self.tile_size = tile_size.text + max_width = image_pyramid.find(f"{self.ns}maxWidth") + if max_width is not None: + self.max_width = max_width.text + max_height = image_pyramid.find(f"{self.ns}maxHeight") + if max_height is not None: + self.max_height = max_height.text + grid_origin = image_pyramid.find(f"{self.ns}gridOrigin") + if grid_origin is not None: + self.grid_origin = grid_origin.text + point = element.find(f"{self.ns}Point") + if point is not None: + self.point = point.text shape = element.find(f"{self.ns}shape") if shape is not None: self.shape = shape.text diff --git a/fastkml/styles.py b/fastkml/styles.py index 26699fb8..008a1ac4 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -26,6 +26,7 @@ from typing import List from typing import Optional from typing import Type +from typing import TypedDict from typing import Union from fastkml import config @@ -138,6 +139,13 @@ def from_element(self, element: Element) -> None: self.color = color.text +class HotSpot(TypedDict): + x: float + y: float + xunits: str # pixels, fraction, insetPixels + yunits: str + + class IconStyle(_ColorStyle): """Specifies how icons for point Placemarks are drawn""" @@ -161,13 +169,13 @@ def __init__( scale: float = 1.0, heading: Optional[float] = None, icon_href: Optional[str] = None, - hotSpot=None, + hot_spot: HotSpot = None, ) -> None: super().__init__(ns, id, target_id, color, color_mode) self.scale = scale self.heading = heading self.icon_href = icon_href - self.hotSpot = hotSpot + self.hot_spot = hot_spot def etree_element(self) -> Element: element = super().etree_element() @@ -193,12 +201,12 @@ def etree_element(self) -> Element: f"{self.ns}href", ) href.text = self.icon_href - if self.hotSpot: - hotSpot = config.etree.SubElement(element, f"{self.ns}hotSpot") - hotSpot.attrib["x"] = str(self.hotSpot["x"]) - hotSpot.attrib["y"] = str(self.hotSpot["y"]) - hotSpot.attrib["xunits"] = str(self.hotSpot["xunits"]) - hotSpot.attrib["yunits"] = str(self.hotSpot["yunits"]) + if self.hot_spot: + hot_spot = config.etree.SubElement(element, f"{self.ns}hotSpot") + hot_spot.attrib["x"] = str(self.hot_spot["x"]) + hot_spot.attrib["y"] = str(self.hot_spot["y"]) + hot_spot.attrib["xunits"] = str(self.hot_spot["xunits"]) + hot_spot.attrib["yunits"] = str(self.hot_spot["yunits"]) return element def from_element(self, element: Element) -> None: @@ -214,13 +222,13 @@ def from_element(self, element: Element) -> None: href = icon.find(f"{self.ns}href") if href is not None: self.icon_href = href.text - hotSpot = element.find(f"{self.ns}hotSpot") - if hotSpot is not None: - self.hotSpot = { - "x": hotSpot.attrib["x"], - "y": hotSpot.attrib["y"], - "xunits": hotSpot.attrib["xunits"], - "yunits": hotSpot.attrib["yunits"], + hot_spot = element.find(f"{self.ns}hotSpot") + if hot_spot is not None: + self.hot_spot: HotSpot = { + "x": hot_spot.attrib["x"], + "y": hot_spot.attrib["y"], + "xunits": hot_spot.attrib["xunits"], + "yunits": hot_spot.attrib["yunits"], } From b0f27b8c77ca5267bb7dfa783f2f7809dca82a90 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 6 Oct 2022 18:14:04 +0100 Subject: [PATCH 122/144] validate easing, etc to be between -180 and 180 --- fastkml/kml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index e3ed0662..ed6dbb04 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -1265,11 +1265,11 @@ def lat_lon_box(self, north, south, east, west, rotation=0): self.east = east else: raise ValueError - if -180 <= float(east) <= 180: + if -180 <= float(west) <= 180: self.west = west else: raise ValueError - if -180 <= float(east) <= 180: + if -180 <= float(rotation) <= 180: self.rotation = rotation else: raise ValueError From cfc9b806327079e6152cb0353619f0b930b0d27e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 6 Oct 2022 18:41:43 +0100 Subject: [PATCH 123/144] import TypedDict from typing_extensions --- fastkml/styles.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastkml/styles.py b/fastkml/styles.py index 008a1ac4..77327fc3 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -26,9 +26,10 @@ from typing import List from typing import Optional from typing import Type -from typing import TypedDict from typing import Union +from typing_extensions import TypedDict + from fastkml import config from fastkml.base import _BaseObject from fastkml.types import Element From af4bffd7bd3fa5c142e40b17a624fc59ea594411 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 6 Oct 2022 18:46:54 +0100 Subject: [PATCH 124/144] fail under 89% coverage --- .github/workflows/run-all-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 9c76f10f..a9509acb 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -44,7 +44,7 @@ jobs: pip install lxml - name: Test with pytest run: | - pytest fastkml --cov=fastkml --cov-fail-under=97 --cov-report=xml + pytest fastkml --cov=fastkml --cov-fail-under=89 --cov-report=xml - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v3 with: From ffb9a3c93649cdf751dc8de4a970dc546ca2f90f Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 6 Oct 2022 18:49:33 +0100 Subject: [PATCH 125/144] fail under 88% coverage --- .github/workflows/run-all-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index a9509acb..8b1fb285 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -44,7 +44,7 @@ jobs: pip install lxml - name: Test with pytest run: | - pytest fastkml --cov=fastkml --cov-fail-under=89 --cov-report=xml + pytest fastkml --cov=fastkml --cov-fail-under=88 --cov-report=xml - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v3 with: From f982cc1d4819e001f4e06a6a87a2b173eacd8301 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 17:39:06 +0100 Subject: [PATCH 126/144] fix tests --- fastkml/kml.py | 552 +++++++++++++++++++++------------- fastkml/tests/kml_test.py | 58 ++++ fastkml/tests/oldunit_test.py | 34 +-- 3 files changed, 409 insertions(+), 235 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 3d53e738..bb066a91 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -28,6 +28,7 @@ import urllib.parse as urlparse from datetime import date from datetime import datetime +from typing import Optional # note that there are some ISO 8601 timeparsers at pypi # but in my tests all of them had some errors so we rely on the @@ -269,15 +270,16 @@ class _Feature(_BaseObject): def __init__( self, - ns=None, - id=None, - name=None, - description=None, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, styles=None, - style_url=None, + style_url: Optional[str] = None, extended_data=None, ): - super().__init__(ns, id) + super().__init__(ns=ns, id=id, target_id=target_id) self.name = name self.description = description self.style_url = style_url @@ -618,6 +620,304 @@ def from_element(self, element): self.camera = s +class Icon(_BaseObject): + """ + Represents an element used in Overlays. + + Defines an image associated with an Icon style or overlay. + The required child element defines the location + of the image to be used as the overlay or as the icon for the placemark. + This location can either be on a local file system or a remote web server. + The , , , and elements are used to select one + icon from an image that contains multiple icons + (often referred to as an icon palette). + """ + + __name__ = "Icon" + + _href = None + _refresh_mode: str = None + _refresh_interval = None + _view_refresh_mode = None + _view_refresh_time = None + _view_bound_scale = None + _view_format = None + _http_query = None + + def __init__( + self, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + href: Optional[str] = None, + refresh_mode: Optional[str] = None, + refresh_interval: Optional[float] = None, + view_refresh_mode: Optional[str] = None, + view_refresh_time: Optional[float] = None, + view_bound_scale: Optional[float] = None, + view_format: Optional[str] = None, + http_query: Optional[str] = None, + ) -> None: + """Initialize the KML Icon Object.""" + super().__init__(ns=ns, id=id, target_id=target_id) + self._href = href + self._refresh_mode = refresh_mode + self._refresh_interval = refresh_interval + self._view_refresh_mode = view_refresh_mode + self._view_refresh_time = view_refresh_time + self._view_bound_scale = view_bound_scale + self._view_format = view_format + self._http_query = http_query + + @property + def href(self) -> Optional[str]: + """An HTTP address or a local file specification used to load an icon.""" + return self._href + + @href.setter + def href(self, href) -> None: + if isinstance(href, str): + self._href = href + elif href is None: + self._href = None + else: + raise ValueError + + @property + def refresh_mode(self) -> Optional[str]: + """ + Specifies a time-based refresh mode. + + Can be one of the following: + - onChange - refresh when the file is loaded and whenever the Link + parameters change (the default). + - onInterval - refresh every n seconds (specified in ). + - onExpire - refresh the file when the expiration time is reached. + """ + return self._refresh_mode + + @refresh_mode.setter + def refresh_mode(self, refresh_mode) -> None: + if isinstance(refresh_mode, str): + self._refresh_mode = refresh_mode + elif refresh_mode is None: + self._refresh_mode = None + else: + raise ValueError + + @property + def refresh_interval(self) -> Optional[float]: + """Indicates to refresh the file every n seconds.""" + return self._refresh_interval + + @refresh_interval.setter + def refresh_interval(self, refresh_interval: float) -> None: + if isinstance(refresh_interval, str): + self._refresh_interval = refresh_interval + elif refresh_interval is None: + self._refresh_interval = None + else: + raise ValueError + + @property + def view_refresh_mode(self): + """ + Specifies how the link is refreshed when the "camera" changes. + + Can be one of the following: + - never (default) - Ignore changes in the view. + Also ignore parameters, if any. + - onStop - Refresh the file n seconds after movement stops, + where n is specified in . + - onRequest - Refresh the file only when the user explicitly requests it. + (For example, in Google Earth, the user right-clicks and selects Refresh + in the Context menu.) + - onRegion - Refresh the file when the Region becomes active. + """ + return self._view_refresh_mode + + @view_refresh_mode.setter + def view_refresh_mode(self, view_refresh_mode): + if isinstance(view_refresh_mode, str): + self._view_refresh_mode = view_refresh_mode + elif view_refresh_mode is None: + self._view_refresh_mode = None + else: + raise ValueError + + @property + def view_refresh_time(self): + """ + After camera movement stops, specifies the number of seconds to + wait before refreshing the view. + """ + return self._view_refresh_time + + @view_refresh_time.setter + def view_refresh_time(self, view_refresh_time): + if isinstance(view_refresh_time, str): + self._view_refresh_time = view_refresh_time + elif view_refresh_time is None: + self._view_refresh_time = None + else: + raise ValueError + + @property + def view_bound_scale(self): + """ + Scales the BBOX parameters before sending them to the server. + + A value less than 1 specifies to use less than the full view (screen). + A value greater than 1 specifies to fetch an area that extends beyond + the edges of the current view. + """ + return self._view_bound_scale + + @view_bound_scale.setter + def view_bound_scale(self, view_bound_scale): + if isinstance(view_bound_scale, str): + self._view_bound_scale = view_bound_scale + elif view_bound_scale is None: + self._view_bound_scale = None + else: + raise ValueError + + @property + def view_format(self): + """ + Specifies the format of the query string that is appended to the + Link's before the file is fetched. + (If the specifies a local file, this element is ignored.) + + This information matches the Web Map Service (WMS) bounding box specification. + If you specify an empty tag, no information is appended to the + query string. + You can also specify a custom set of viewing parameters to add to the query + string. If you supply a format string, it is used instead of the BBOX + information. If you also want the BBOX information, you need to add those + parameters along with the custom parameters. + You can use any of the following parameters in your format string: + - [lookatLon], [lookatLat] - longitude and latitude of the point that + is viewing + - [lookatRange], [lookatTilt], [lookatHeading] - values used by the + element (see descriptions of , , and + in ) + - [lookatTerrainLon], [lookatTerrainLat], [lookatTerrainAlt] - point on the + terrain in degrees/meters that is viewing + - [cameraLon], [cameraLat], [cameraAlt] - degrees/meters of the eyepoint for + the camera + - [horizFov], [vertFov] - horizontal, vertical field of view for the camera + - [horizPixels], [vertPixels] - size in pixels of the 3D viewer + - [terrainEnabled] - indicates whether the 3D viewer is showing terrain (1) + or not (0) + """ + return self._view_format + + @view_format.setter + def view_format(self, view_format): + if isinstance(view_format, str): + self._view_format = view_format + elif view_format is None: + self._view_format = None + else: + raise ValueError + + @property + def http_query(self): + """ + Appends information to the query string, based on the parameters specified. + + The following parameters are supported: + - [clientVersion] + - [kmlVersion] + - [clientName] + - [language] + """ + return self._http_query + + @http_query.setter + def http_query(self, http_query): + if isinstance(http_query, str): + self._http_query = http_query + elif http_query is None: + self._http_query = None + else: + raise ValueError + + def etree_element(self): + element = super(Icon, self).etree_element() + + if self._href: + href = config.etree.SubElement(element, f"{self.ns}href") + href.text = self._href + if self._refresh_mode: + refresh_mode = config.etree.SubElement(element, f"{self.ns}refreshMode") + refresh_mode.text = self._refresh_mode + if self._refresh_interval: + refresh_interval = config.etree.SubElement( + element, f"{self.ns}refreshInterval" + ) + refresh_interval.text = self._refresh_interval + if self._view_refresh_mode: + view_refresh_mode = config.etree.SubElement( + element, f"{self.ns}viewRefreshMode" + ) + view_refresh_mode.text = self._view_refresh_mode + if self._view_refresh_time: + view_refresh_time = config.etree.SubElement( + element, f"{self.ns}viewRefreshTime" + ) + view_refresh_time.text = self._view_refresh_time + if self._view_bound_scale: + view_bound_scale = config.etree.SubElement( + element, f"{self.ns}viewBoundScale" + ) + view_bound_scale.text = self._view_bound_scale + if self._view_format: + view_format = config.etree.SubElement(element, f"{self.ns}viewFormat") + view_format.text = self._view_format + if self._http_query: + http_query = config.etree.SubElement(element, f"{self.ns}httpQuery") + http_query.text = self._http_query + + return element + + def from_element(self, element): + super().from_element(element) + + href = element.find(f"{self.ns}href") + if href is not None: + self.href = href.text + + refresh_mode = element.find(f"{self.ns}refreshMode") + if refresh_mode is not None: + self.refresh_mode = refresh_mode.text + + refresh_interval = element.find(f"{self.ns}refreshInterval") + if refresh_interval is not None: + self.refresh_interval = refresh_interval.text + + view_refresh_mode = element.find(f"{self.ns}viewRefreshMode") + if view_refresh_mode is not None: + self.view_refresh_mode = view_refresh_mode.text + + view_refresh_time = element.find(f"{self.ns}viewRefreshTime") + if view_refresh_time is not None: + self.view_refresh_time = view_refresh_time.text + + view_bound_scale = element.find(f"{self.ns}viewBoundScale") + if view_bound_scale is not None: + self.view_bound_scale = view_bound_scale.text + + view_format = element.find(f"{self.ns}viewFormat") + if view_format is not None: + self.view_format = view_format.text + + http_query = element.find(f"{self.ns}httpQuery") + if http_query is not None: + self.http_query = http_query.text + + class _Container(_Feature): """ abstract element; do not create @@ -631,10 +931,26 @@ class _Container(_Feature): _features = [] def __init__( - self, ns=None, id=None, name=None, description=None, styles=None, style_url=None + self, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + styles=None, + style_url: Optional[str] = None, + features=None, ): - super().__init__(ns, id, name, description, styles, style_url) - self._features = [] + super().__init__( + ns=ns, + id=id, + target_id=target_id, + name=name, + description=description, + styles=styles, + style_url=style_url, + ) + self._features = features or [] def features(self): """iterate over features""" @@ -694,9 +1010,26 @@ class _Overlay(_Feature): # the color and size defined by the ground or screen overlay. def __init__( - self, ns=None, id=None, name=None, description=None, styles=None, style_url=None + self, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + styles=None, + style_url: Optional[str] = None, + icon: Optional[Icon] = None, ): - super().__init__(ns, id, name, description, styles, style_url) + super().__init__( + ns=ns, + id=id, + target_id=target_id, + name=name, + description=description, + styles=styles, + style_url=style_url, + ) + self._icon = icon @property def color(self): @@ -746,8 +1079,7 @@ def etree_element(self): draw_order = config.etree.SubElement(element, f"{self.ns}drawOrder") draw_order.text = self._draw_order if self._icon: - icon = config.etree.SubElement(element, f"{self.ns}icon") - icon.text = self._icon + element.append(self._icon.etree_element()) return element def from_element(self, element): @@ -758,7 +1090,7 @@ def from_element(self, element): draw_order = element.find(f"{self.ns}drawOrder") if draw_order is not None: self.draw_order = draw_order.text - icon = element.find(f"{self.ns}icon") + icon = element.find(f"{self.ns}Icon") if icon is not None: s = Icon(self.ns) s.from_element(icon) @@ -1896,198 +2228,6 @@ def from_element(self, element): self.append_data(sd.get("name"), sd.text) -class Icon(_BaseObject): - """Represents an element used in Overlays""" - - __name__ = "Icon" - - _href = None - _refresh_mode = None - _refresh_interval = None - _view_refresh_mode = None - _view_refresh_time = None - _view_bound_scale = None - _view_format = None - _http_query = None - - @property - def href(self): - return self._href - - @href.setter - def href(self, href): - if isinstance(href, str): - self._href = href - elif href is None: - self._href = None - else: - raise ValueError - - @property - def refresh_mode(self): - return self._refresh_mode - - @refresh_mode.setter - def refresh_mode(self, refresh_mode): - if isinstance(refresh_mode, str): - self._refresh_mode = refresh_mode - elif refresh_mode is None: - self._refresh_mode = None - else: - raise ValueError - - @property - def refresh_interval(self): - return self._refresh_interval - - @refresh_interval.setter - def refresh_interval(self, refresh_interval): - if isinstance(refresh_interval, str): - self._refresh_interval = refresh_interval - elif refresh_interval is None: - self._refresh_interval = None - else: - raise ValueError - - @property - def view_refresh_mode(self): - return self._view_refresh_mode - - @view_refresh_mode.setter - def view_refresh_mode(self, view_refresh_mode): - if isinstance(view_refresh_mode, str): - self._view_refresh_mode = view_refresh_mode - elif view_refresh_mode is None: - self._view_refresh_mode = None - else: - raise ValueError - - @property - def view_refresh_time(self): - return self._view_refresh_time - - @view_refresh_time.setter - def view_refresh_time(self, view_refresh_time): - if isinstance(view_refresh_time, str): - self._view_refresh_time = view_refresh_time - elif view_refresh_time is None: - self._view_refresh_time = None - else: - raise ValueError - - @property - def view_bound_scale(self): - return self._view_bound_scale - - @view_bound_scale.setter - def view_bound_scale(self, view_bound_scale): - if isinstance(view_bound_scale, str): - self._view_bound_scale = view_bound_scale - elif view_bound_scale is None: - self._view_bound_scale = None - else: - raise ValueError - - @property - def view_format(self): - return self._view_format - - @view_format.setter - def view_format(self, view_format): - if isinstance(view_format, str): - self._view_format = view_format - elif view_format is None: - self._view_format = None - else: - raise ValueError - - @property - def http_query(self): - return self._http_query - - @http_query.setter - def http_query(self, http_query): - if isinstance(http_query, str): - self._http_query = http_query - elif http_query is None: - self._http_query = None - else: - raise ValueError - - def etree_element(self): - element = super(Icon, self).etree_element() - - if self._href: - href = config.etree.SubElement(element, f"{self.ns}href") - href.text = self._href - if self._refresh_mode: - refresh_mode = config.etree.SubElement(element, f"{self.ns}refreshMode") - refresh_mode.text = self._refresh_mode - if self._refresh_interval: - refresh_interval = config.etree.SubElement( - element, f"{self.ns}refreshInterval" - ) - refresh_interval.text = self._refresh_interval - if self._view_refresh_mode: - view_refresh_mode = config.etree.SubElement( - element, f"{self.ns}viewRefreshMode" - ) - view_refresh_mode.text = self._view_refresh_mode - if self._view_refresh_time: - view_refresh_time = config.etree.SubElement( - element, f"{self.ns}viewRefreshTime" - ) - view_refresh_time.text = self._view_refresh_time - if self._view_bound_scale: - view_bound_scale = config.etree.SubElement( - element, f"{self.ns}viewBoundScale" - ) - view_bound_scale.text = self._view_bound_scale - if self._view_format: - view_format = config.etree.SubElement(element, f"{self.ns}viewFormat") - view_format.text = self._view_format - if self._http_query: - http_query = config.etree.SubElement(element, f"{self.ns}httpQuery") - http_query.text = self._http_query - - return element - - def from_element(self, element): - super(Icon, self).from_element(element) - - href = element.find(f"{self.ns}href") - if href is not None: - self.href = href.text - - refresh_mode = element.find(f"{self.ns}refreshMode") - if refresh_mode is not None: - self.refresh_mode = refresh_mode.text - - refresh_interval = element.find(f"{self.ns}refreshInterval") - if refresh_interval is not None: - self.refresh_interval = refresh_interval.text - - view_refresh_mode = element.find(f"{self.ns}viewRefreshMode") - if view_refresh_mode is not None: - self.view_refresh_mode = view_refresh_mode.text - - view_refresh_time = element.find(f"{self.ns}viewRefreshTime") - if view_refresh_time is not None: - self.view_refresh_time = view_refresh_time.text - - view_bound_scale = element.find(f"{self.ns}viewBoundScale") - if view_bound_scale is not None: - self.view_bound_scale = view_bound_scale.text - - view_format = element.find(f"{self.ns}viewFormat") - if view_format is not None: - self.view_format = view_format.text - - http_query = element.find(f"{self.ns}httpQuery") - if http_query is not None: - self.http_query = http_query.text - - class _AbstractView(_BaseObject): """ This is an abstract element and cannot be used directly in a KML file. diff --git a/fastkml/tests/kml_test.py b/fastkml/tests/kml_test.py index bb506d04..f4e8a8d8 100644 --- a/fastkml/tests/kml_test.py +++ b/fastkml/tests/kml_test.py @@ -15,6 +15,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Test the kml classes.""" +from fastkml import kml from fastkml.tests.base import Lxml from fastkml.tests.base import StdLibrary @@ -22,6 +23,63 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" + def test_icon(self): + """Test the Icon class.""" + icon = kml.Icon( + id="icon-01", + href="http://maps.google.com/mapfiles/kml/paddle/red-circle.png", + refresh_mode="onInterval", + refresh_interval="60", + view_refresh_mode="onStop", + view_refresh_time="4", + view_bound_scale="1.2", + view_format="BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]", + http_query="clientName=fastkml", + ) + + assert icon.id == "icon-01" + assert icon.href == "http://maps.google.com/mapfiles/kml/paddle/red-circle.png" + assert icon.refresh_mode == "onInterval" + assert icon.refresh_interval == "60" + assert icon.view_refresh_mode == "onStop" + assert icon.view_refresh_time == "4" + assert icon.view_bound_scale == "1.2" + assert icon.view_format == "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" + assert icon.http_query == "clientName=fastkml" + + def test_icon_read(self) -> None: + """Test the Icon class.""" + icon = kml.Icon() + icon.from_string( + """ + + http://maps.google.com/mapfiles/kml/paddle/red-circle.png + onInterval + 60 + onStop + 4 + 1.2 + BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth] + clientName=fastkml + + """.strip() + ) + + assert icon.id == "icon-01" + assert icon.href == "http://maps.google.com/mapfiles/kml/paddle/red-circle.png" + assert icon.refresh_mode == "onInterval" + assert icon.refresh_interval == "60" + assert icon.view_refresh_mode == "onStop" + assert icon.view_refresh_time == "4" + assert icon.view_bound_scale == "1.2" + assert icon.view_format == "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" + assert icon.http_query == "clientName=fastkml" + + icon2 = kml.Icon() + icon2.from_string(icon.to_string()) + + assert icon2.to_string() == icon.to_string() + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" diff --git a/fastkml/tests/oldunit_test.py b/fastkml/tests/oldunit_test.py index 002786d0..9070ecbe 100644 --- a/fastkml/tests/oldunit_test.py +++ b/fastkml/tests/oldunit_test.py @@ -2226,33 +2226,6 @@ def test_draw_order_value_error(self): with pytest.raises(ValueError): o.draw_order = object() - def test_icon_without_tag(self): - o = kml._Overlay(name="An Overlay") - o.icon = "http://example.com/" - assert o.icon == "http://example.com/" - - def test_icon_with_open_tag(self): - o = kml._Overlay(name="An Overlay") - o.icon = "http://example.com/" - assert o.icon == "http://example.com/" - - def test_icon_with_close_tag(self): - o = kml._Overlay(name="An Overlay") - o.icon = "http://example.com/" - assert o.icon == "http://example.com/" - - def test_icon_with_tag(self): - o = kml._Overlay(name="An Overlay") - o.icon = "http://example.com/" - assert o.icon == "http://example.com/" - - def test_icon_to_none(self): - o = kml._Overlay(name="An Overlay") - o.icon = "http://example.com/" - assert o.icon == "http://example.com/" - o.icon = None - assert o.icon is None - def test_icon_raise_exception(self): o = kml._Overlay(name="An Overlay") with pytest.raises(ValueError): @@ -2411,7 +2384,8 @@ def test_default_to_string(self): def test_to_string(self): g = kml.GroundOverlay() - g.icon = "http://example.com" + icon = kml.Icon(href="http://example.com") + g.icon = icon g.draw_order = 1 g.color = "00010203" @@ -2421,7 +2395,9 @@ def test_to_string(self): "1" "00010203" "1" - "<href>http://example.com</href>" + "" + "http://example.com" + "" "" ) From 2b82934f842b5e5aabdd5e831f2da1e1f4be1974 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 18:02:54 +0100 Subject: [PATCH 127/144] fix type annotations --- fastkml/kml.py | 35 ++++++++++++++++++++++------------- fastkml/tests/kml_test.py | 20 ++++++++++---------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index bb066a91..27ac0c3c 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -711,8 +711,8 @@ def refresh_interval(self) -> Optional[float]: return self._refresh_interval @refresh_interval.setter - def refresh_interval(self, refresh_interval: float) -> None: - if isinstance(refresh_interval, str): + def refresh_interval(self, refresh_interval: Optional[float]) -> None: + if isinstance(refresh_interval, float): self._refresh_interval = refresh_interval elif refresh_interval is None: self._refresh_interval = None @@ -754,8 +754,8 @@ def view_refresh_time(self): return self._view_refresh_time @view_refresh_time.setter - def view_refresh_time(self, view_refresh_time): - if isinstance(view_refresh_time, str): + def view_refresh_time(self, view_refresh_time: Optional[float]): + if isinstance(view_refresh_time, float): self._view_refresh_time = view_refresh_time elif view_refresh_time is None: self._view_refresh_time = None @@ -763,7 +763,7 @@ def view_refresh_time(self, view_refresh_time): raise ValueError @property - def view_bound_scale(self): + def view_bound_scale(self) -> Optional[float]: """ Scales the BBOX parameters before sending them to the server. @@ -774,8 +774,8 @@ def view_bound_scale(self): return self._view_bound_scale @view_bound_scale.setter - def view_bound_scale(self, view_bound_scale): - if isinstance(view_bound_scale, str): + def view_bound_scale(self, view_bound_scale: Optional[float]) -> None: + if isinstance(view_bound_scale, float): self._view_bound_scale = view_bound_scale elif view_bound_scale is None: self._view_bound_scale = None @@ -857,7 +857,7 @@ def etree_element(self): refresh_interval = config.etree.SubElement( element, f"{self.ns}refreshInterval" ) - refresh_interval.text = self._refresh_interval + refresh_interval.text = str(self._refresh_interval) if self._view_refresh_mode: view_refresh_mode = config.etree.SubElement( element, f"{self.ns}viewRefreshMode" @@ -867,12 +867,12 @@ def etree_element(self): view_refresh_time = config.etree.SubElement( element, f"{self.ns}viewRefreshTime" ) - view_refresh_time.text = self._view_refresh_time + view_refresh_time.text = str(self._view_refresh_time) if self._view_bound_scale: view_bound_scale = config.etree.SubElement( element, f"{self.ns}viewBoundScale" ) - view_bound_scale.text = self._view_bound_scale + view_bound_scale.text = str(self._view_bound_scale) if self._view_format: view_format = config.etree.SubElement(element, f"{self.ns}viewFormat") view_format.text = self._view_format @@ -895,7 +895,10 @@ def from_element(self, element): refresh_interval = element.find(f"{self.ns}refreshInterval") if refresh_interval is not None: - self.refresh_interval = refresh_interval.text + try: + self.refresh_interval = float(refresh_interval.text) + except ValueError: + self.refresh_interval = None view_refresh_mode = element.find(f"{self.ns}viewRefreshMode") if view_refresh_mode is not None: @@ -903,11 +906,17 @@ def from_element(self, element): view_refresh_time = element.find(f"{self.ns}viewRefreshTime") if view_refresh_time is not None: - self.view_refresh_time = view_refresh_time.text + try: + self.view_refresh_time = float(view_refresh_time.text) + except ValueError: + self.view_refresh_time = None view_bound_scale = element.find(f"{self.ns}viewBoundScale") if view_bound_scale is not None: - self.view_bound_scale = view_bound_scale.text + try: + self.view_bound_scale = float(view_bound_scale.text) + except ValueError: + self.view_bound_scale = None view_format = element.find(f"{self.ns}viewFormat") if view_format is not None: diff --git a/fastkml/tests/kml_test.py b/fastkml/tests/kml_test.py index f4e8a8d8..f8bd6d96 100644 --- a/fastkml/tests/kml_test.py +++ b/fastkml/tests/kml_test.py @@ -23,16 +23,16 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" - def test_icon(self): + def test_icon(self) -> None: """Test the Icon class.""" icon = kml.Icon( id="icon-01", href="http://maps.google.com/mapfiles/kml/paddle/red-circle.png", refresh_mode="onInterval", - refresh_interval="60", + refresh_interval=60, view_refresh_mode="onStop", - view_refresh_time="4", - view_bound_scale="1.2", + view_refresh_time=4, + view_bound_scale=1.2, view_format="BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]", http_query="clientName=fastkml", ) @@ -40,10 +40,10 @@ def test_icon(self): assert icon.id == "icon-01" assert icon.href == "http://maps.google.com/mapfiles/kml/paddle/red-circle.png" assert icon.refresh_mode == "onInterval" - assert icon.refresh_interval == "60" + assert icon.refresh_interval == 60 assert icon.view_refresh_mode == "onStop" - assert icon.view_refresh_time == "4" - assert icon.view_bound_scale == "1.2" + assert icon.view_refresh_time == 4 + assert icon.view_bound_scale == 1.2 assert icon.view_format == "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" assert icon.http_query == "clientName=fastkml" @@ -68,10 +68,10 @@ def test_icon_read(self) -> None: assert icon.id == "icon-01" assert icon.href == "http://maps.google.com/mapfiles/kml/paddle/red-circle.png" assert icon.refresh_mode == "onInterval" - assert icon.refresh_interval == "60" + assert icon.refresh_interval == 60 assert icon.view_refresh_mode == "onStop" - assert icon.view_refresh_time == "4" - assert icon.view_bound_scale == "1.2" + assert icon.view_refresh_time == 4 + assert icon.view_bound_scale == 1.2 assert icon.view_format == "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" assert icon.http_query == "clientName=fastkml" From 89403ccd91fac2668ae5af94ecaf6c7636c3a0e2 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 18:05:57 +0100 Subject: [PATCH 128/144] Update fastkml/kml.py Co-authored-by: code-review-doctor[bot] <72320148+code-review-doctor[bot]@users.noreply.github.com> --- fastkml/kml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 27ac0c3c..32c16e38 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -845,7 +845,7 @@ def http_query(self, http_query): raise ValueError def etree_element(self): - element = super(Icon, self).etree_element() + element = super().etree_element() if self._href: href = config.etree.SubElement(element, f"{self.ns}href") From 57f957c8d474051944c871c67bf0e7bb89c7196e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 19:08:58 +0100 Subject: [PATCH 129/144] #133 type annotate geometries --- fastkml/geometry.py | 110 ++++++++++++++++++++++++++++++-------------- pyproject.toml | 2 +- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 7292cda3..3d6f4eec 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -139,8 +139,8 @@ def _set_altitude_mode(self, element: Element) -> None: "absolute", ] if self.altitude_mode != "clampToGround": - am_element = config.etree.SubElement( - element, f"{self.ns}altitudeMode" # type: ignore[arg-type] + am_element = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}altitudeMode" ) am_element.text = self.altitude_mode @@ -152,8 +152,8 @@ def _set_extrude(self, element: Element) -> None: ]: et_element = cast( Element, - config.etree.SubElement( - element, f"{self.ns}extrude" # type: ignore[arg-type] + config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}extrude" ), ) et_element.text = "1" @@ -162,7 +162,10 @@ def _etree_coordinates( self, coordinates: Sequence[PointType], ) -> Element: - element = cast(Element, config.etree.Element(f"{self.ns}coordinates")) + element = cast( + Element, + config.etree.Element(f"{self.ns}coordinates"), # type: ignore[attr-defined] + ) if len(coordinates[0]) == 2: if config.FORCE3D: # and not clampToGround: tuples = (f"{c[0]:f},{c[1]:f},0.000000" for c in coordinates) @@ -187,8 +190,8 @@ def _etree_linestring(self, linestring: LineString) -> Element: "clampToGround", "clampToSeaFloor", ]: - ts_element = config.etree.SubElement( - element, f"{self.ns}tessellate" # type: ignore[arg-type] + ts_element = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}tessellate" ) ts_element.text = "1" return self._extracted_from__etree_linearring_5(linestring, element) @@ -208,8 +211,8 @@ def _etree_polygon(self, polygon: Polygon) -> Element: element = self._extrude_and_altitude_mode("Polygon") outer_boundary = cast( Element, - config.etree.SubElement( - element, # type: ignore[arg-type] + config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}outerBoundaryIs", ), ) @@ -217,8 +220,8 @@ def _etree_polygon(self, polygon: Polygon) -> Element: for ib in polygon.interiors: inner_boundary = cast( Element, - config.etree.SubElement( - element, # type: ignore[arg-type] + config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}innerBoundaryIs", ), ) @@ -226,40 +229,65 @@ def _etree_polygon(self, polygon: Polygon) -> Element: return element def _extrude_and_altitude_mode(self, kml_geometry: str) -> Element: - result = cast(Element, config.etree.Element(f"{self.ns}{kml_geometry}")) + result = cast( + Element, + config.etree.Element( # type: ignore[attr-defined] + f"{self.ns}{kml_geometry}" + ), + ) self._set_extrude(result) self._set_altitude_mode(result) return result def _etree_multipoint(self, points: MultiPoint) -> Element: - element = cast(Element, config.etree.Element(f"{self.ns}MultiGeometry")) + element = cast( + Element, + config.etree.Element( # type: ignore[attr-defined] + f"{self.ns}MultiGeometry" + ), + ) for point in points.geoms: element.append(self._etree_point(point)) return element def _etree_multilinestring(self, linestrings: MultiLineString) -> Element: - element = cast(Element, config.etree.Element(f"{self.ns}MultiGeometry")) + element = cast( + Element, + config.etree.Element( # type: ignore[attr-defined] + f"{self.ns}MultiGeometry" + ), + ) for linestring in linestrings.geoms: element.append(self._etree_linestring(linestring)) return element def _etree_multipolygon(self, polygons: MultiPolygon) -> Element: - element = cast(Element, config.etree.Element(f"{self.ns}MultiGeometry")) + element = cast( + Element, + config.etree.Element( # type: ignore[attr-defined] + f"{self.ns}MultiGeometry" + ), + ) for polygon in polygons.geoms: element.append(self._etree_polygon(polygon)) return element def _etree_collection(self, features: GeometryCollection) -> Element: - element = cast(Element, config.etree.Element(f"{self.ns}MultiGeometry")) + element = cast( + Element, + config.etree.Element( # type: ignore[attr-defined] + f"{self.ns}MultiGeometry" + ), + ) for feature in features.geoms: if feature.geom_type == "Point": - element.append(self._etree_point(feature)) + element.append(self._etree_point(cast(Point, feature))) elif feature.geom_type == "LinearRing": - element.append(self._etree_linearring(feature)) + element.append(self._etree_linearring(cast(LinearRing, feature))) elif feature.geom_type == "LineString": - element.append(self._etree_linestring(feature)) + element.append(self._etree_linestring(cast(LineString, feature))) elif feature.geom_type == "Polygon": - element.append(self._etree_polygon(feature)) + element.append(self._etree_polygon(cast(Polygon, feature))) else: raise ValueError("Illegal geometry type.") return element @@ -295,7 +323,7 @@ def _get_geometry_spec(self, element: Element) -> None: et = False self.extrude = et else: - self.extrude = False + self.extrude = False # type: ignore[unreachable] tessellate = element.find(f"{self.ns}tessellate") if tessellate is not None: try: @@ -304,7 +332,7 @@ def _get_geometry_spec(self, element: Element) -> None: te = False self.tessellate = te else: - self.tessellate = False + self.tessellate = False # type: ignore[unreachable] altitude_mode = element.find(f"{self.ns}altitudeMode") if altitude_mode is not None: am = altitude_mode.text.strip() @@ -318,7 +346,7 @@ def _get_geometry_spec(self, element: Element) -> None: else: self.altitude_mode = None else: - self.altitude_mode = None + self.altitude_mode = None # type: ignore[unreachable] def _get_coordinates(self, element: Element) -> List[PointType]: coordinates = element.find(f"{self.ns}coordinates") @@ -341,7 +369,7 @@ def _get_linear_ring(self, element: Element) -> Optional[LinearRing]: if lr is not None: coords = self._get_coordinates(lr) return LinearRing(coords) - return None + return None # type: ignore[unreachable] def _get_geometry(self, element: Element) -> Optional[GeometryType]: # Point, LineString, @@ -372,9 +400,9 @@ def _get_geometry(self, element: Element) -> Optional[GeometryType]: return LinearRing(coords) return None - def _get_multigeometry(self, element: Element) -> Optional[GeometryType]: + def _get_multigeometry(self, element: Element) -> Optional[MultiGeometryType]: # MultiGeometry - geoms: List[AnyGeometryType] = [] + geoms: List[Union[AnyGeometryType, None]] = [] if element.tag == f"{self.ns}MultiGeometry": points = element.findall(f"{self.ns}Point") for point in points: @@ -389,29 +417,43 @@ def _get_multigeometry(self, element: Element) -> Optional[GeometryType]: self._get_geometry_spec(polygon) outer_boundary = polygon.find(f"{self.ns}outerBoundaryIs") ob = self._get_linear_ring(outer_boundary) + if not ob: + continue inner_boundaries = polygon.findall(f"{self.ns}innerBoundaryIs") - ibs = [ + inner_bs = [ self._get_linear_ring(inner_boundary) for inner_boundary in inner_boundaries ] + ibs: List[LinearRing] = [ib for ib in inner_bs if ib] geoms.append(Polygon.from_linear_rings(ob, *ibs)) linearings = element.findall(f"{self.ns}LinearRing") if linearings: for lr in linearings: self._get_geometry_spec(lr) geoms.append(LinearRing(self._get_coordinates(lr))) - if geoms: - geom_types = {geom.geom_type for geom in geoms} + clean_geoms: List[AnyGeometryType] = [g for g in geoms if g] + if clean_geoms: + geom_types = {geom.geom_type for geom in clean_geoms} if len(geom_types) > 1: - return GeometryCollection(geoms) + return GeometryCollection( + clean_geoms, # type: ignore[arg-type] + ) if "Point" in geom_types: - return MultiPoint.from_points(*geoms) + return MultiPoint.from_points( + *clean_geoms, # type: ignore[arg-type] + ) elif "LineString" in geom_types: - return MultiLineString.from_linestrings(*geoms) + return MultiLineString.from_linestrings( + *clean_geoms, # type: ignore[arg-type] + ) elif "Polygon" in geom_types: - return MultiPolygon.from_polygons(*geoms) + return MultiPolygon.from_polygons( + *clean_geoms, # type: ignore[arg-type] + ) elif "LinearRing" in geom_types: - return GeometryCollection(geoms) + return GeometryCollection( + clean_geoms, # type: ignore[arg-type] + ) return None def from_element(self, element: Element) -> None: diff --git a/pyproject.toml b/pyproject.toml index 2650c6b0..685d87c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,6 @@ show_error_codes = true [[tool.mypy.overrides]] module = [ "fastkml.kml", "fastkml.gx", - "fastkml.geometry", "fastkml.styles", + "fastkml.styles", "fastkml.tests.oldunit_test", "fastkml.tests.config_test"] ignore_errors = true From 94ce71f2820c57fea33e69d739d02362f8f04614 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 19:54:51 +0100 Subject: [PATCH 130/144] #133 type annotate gx --- fastkml/gx.py | 41 ++++++++++++----- fastkml/styles.py | 109 +++++++++++++++++++++++++--------------------- pyproject.toml | 3 +- 3 files changed, 91 insertions(+), 62 deletions(-) diff --git a/fastkml/gx.py b/fastkml/gx.py index b1a710fd..c37f37dc 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -77,13 +77,19 @@ """ import logging +from typing import List +from typing import Optional +from typing import Union +from typing import cast from pygeoif.geometry import GeometryCollection from pygeoif.geometry import LineString from pygeoif.geometry import MultiLineString +from pygeoif.types import PointType -from .config import GXNS as NS -from .geometry import Geometry +from fastkml.config import GXNS as NS +from fastkml.geometry import Geometry +from fastkml.types import Element logger = logging.getLogger(__name__) @@ -91,9 +97,9 @@ class GxGeometry(Geometry): def __init__( self, - ns=None, - id=None, - ): + ns: None = None, + id: None = None, + ) -> None: """ gxgeometry: a read-only subclass of geometry supporting gx: features, like gx:Track @@ -101,34 +107,47 @@ def __init__( super().__init__(ns, id) self.ns = NS if ns is None else ns - def _get_geometry(self, element): + def _get_geometry(self, element: Element) -> Optional[LineString]: # Track if element.tag == (f"{self.ns}Track"): coords = self._get_coordinates(element) self._get_geometry_spec(element) - return LineString(coords) + return LineString( + coords, + ) + return None - def _get_multigeometry(self, element): + def _get_multigeometry( + self, + element: Element, + ) -> Union[MultiLineString, GeometryCollection, None]: # MultiTrack geoms = [] if element.tag == (f"{self.ns}MultiTrack"): tracks = element.findall(f"{self.ns}Track") for track in tracks: self._get_geometry_spec(track) - geoms.append(LineString(self._get_coordinates(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.from_linestrings(*geoms) + return None - def _get_coordinates(self, element): + def _get_coordinates(self, element: Element) -> List[PointType]: coordinates = element.findall(f"{self.ns}coord") if coordinates is not None: return [ - [float(c) for c in coord.text.strip().split()] for coord in coordinates + cast(PointType, tuple(float(c) for c in coord.text.strip().split())) + for coord in coordinates ] + return [] # type: ignore[unreachable] __all__ = ["GxGeometry"] diff --git a/fastkml/styles.py b/fastkml/styles.py index 77327fc3..5df7a08c 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -54,7 +54,7 @@ def __init__( target_id: Optional[str] = None, url: Optional[str] = None, ) -> None: - super().__init__(ns, id, target_id) + super().__init__(ns=ns, id=id, target_id=target_id) self.url = url def etree_element(self) -> Element: @@ -109,21 +109,21 @@ def __init__( color: Optional[str] = None, color_mode: Optional[str] = None, ) -> None: - super().__init__(ns, id, target_id) + super().__init__(ns=ns, id=id, target_id=target_id) self.color = color self.color_mode = color_mode def etree_element(self) -> Element: element = super().etree_element() if self.color: - color = config.etree.SubElement( - element, # type: ignore[arg-type] + color = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}color", ) color.text = self.color if self.color_mode: - color_mode = config.etree.SubElement( - element, # type: ignore[arg-type] + color_mode = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}colorMode", ) color_mode.text = self.color_mode @@ -170,9 +170,12 @@ def __init__( scale: float = 1.0, heading: Optional[float] = None, icon_href: Optional[str] = None, - hot_spot: HotSpot = None, + hot_spot: Optional[HotSpot] = None, ) -> None: - super().__init__(ns, id, target_id, color, color_mode) + super().__init__( + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ) + self.scale = scale self.heading = heading self.icon_href = icon_href @@ -181,29 +184,31 @@ def __init__( def etree_element(self) -> Element: element = super().etree_element() if self.scale is not None: - scale = config.etree.SubElement( - element, # type: ignore[arg-type] + scale = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}scale", ) scale.text = str(self.scale) if self.heading is not None: - heading = config.etree.SubElement( - element, # type: ignore[arg-type] + heading = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}heading", ) heading.text = str(self.heading) if self.icon_href: - icon = config.etree.SubElement( - element, # type: ignore[arg-type] + icon = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}Icon", ) - href = config.etree.SubElement( + href = config.etree.SubElement( # type: ignore[attr-defined] icon, f"{self.ns}href", ) href.text = self.icon_href if self.hot_spot: - hot_spot = config.etree.SubElement(element, f"{self.ns}hotSpot") + hot_spot = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}hotSpot" + ) hot_spot.attrib["x"] = str(self.hot_spot["x"]) hot_spot.attrib["y"] = str(self.hot_spot["y"]) hot_spot.attrib["xunits"] = str(self.hot_spot["xunits"]) @@ -225,11 +230,11 @@ def from_element(self, element: Element) -> None: self.icon_href = href.text hot_spot = element.find(f"{self.ns}hotSpot") if hot_spot is not None: - self.hot_spot: HotSpot = { - "x": hot_spot.attrib["x"], - "y": hot_spot.attrib["y"], - "xunits": hot_spot.attrib["xunits"], - "yunits": hot_spot.attrib["yunits"], + self.hot_spot: HotSpot = { # type: ignore[no-redef] + "x": hot_spot.attrib["x"], # type: ignore[attr-defined] + "y": hot_spot.attrib["y"], # type: ignore[attr-defined] + "xunits": hot_spot.attrib["xunits"], # type: ignore[attr-defined] + "yunits": hot_spot.attrib["yunits"], # type: ignore[attr-defined] } @@ -254,14 +259,16 @@ def __init__( color_mode: Optional[str] = None, width: Union[int, float] = 1, ) -> None: - super().__init__(ns, id, target_id, color, color_mode) + super().__init__( + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ) self.width = width def etree_element(self) -> Element: element = super().etree_element() if self.width is not None: - width = config.etree.SubElement( - element, # type: ignore[arg-type] + width = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}width", ) width.text = str(self.width) @@ -298,21 +305,23 @@ def __init__( fill: int = 1, outline: int = 1, ) -> None: - super().__init__(ns, id, target_id, color, color_mode) + super().__init__( + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ) self.fill = fill self.outline = outline def etree_element(self) -> Element: element = super().etree_element() if self.fill is not None: - fill = config.etree.SubElement( - element, # type: ignore[arg-type] + fill = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}fill", ) fill.text = str(self.fill) if self.outline is not None: - outline = config.etree.SubElement( - element, # type: ignore[arg-type] + outline = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}outline", ) outline.text = str(self.outline) @@ -354,14 +363,16 @@ def __init__( color_mode: Optional[str] = None, scale: float = 1.0, ) -> None: - super().__init__(ns, id, target_id, color, color_mode) + super().__init__( + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ) self.scale = scale def etree_element(self) -> Element: element = super().etree_element() if self.scale is not None: - scale = config.etree.SubElement( - element, # type: ignore[arg-type] + scale = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}scale", ) scale.text = str(self.scale) @@ -435,7 +446,7 @@ def __init__( text: Optional[str] = None, display_mode: Optional[str] = None, ) -> None: - super().__init__(ns, id, target_id) + super().__init__(ns=ns, id=id, target_id=target_id) self.bg_color = bg_color self.text_color = text_color self.text = text @@ -463,26 +474,26 @@ def from_element(self, element: Element) -> None: def etree_element(self) -> Element: element = super().etree_element() if self.bg_color is not None: - elem = config.etree.SubElement( - element, # type: ignore[arg-type] + elem = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}bgColor", ) elem.text = self.bg_color if self.text_color is not None: - elem = config.etree.SubElement( - element, # type: ignore[arg-type] + elem = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}textColor", ) elem.text = self.text_color if self.text is not None: - elem = config.etree.SubElement( - element, # type: ignore[arg-type] + elem = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}text", ) elem.text = self.text if self.display_mode is not None: - elem = config.etree.SubElement( - element, # type: ignore[arg-type] + elem = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}displayMode", ) elem.text = self.display_mode @@ -611,27 +622,27 @@ def from_element(self, element: Element) -> None: def etree_element(self) -> Element: element = super().etree_element() if self.normal and isinstance(self.normal, (Style, StyleUrl)): - pair = config.etree.SubElement( - element, # type: ignore[arg-type] + pair = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}Pair", ) - key = config.etree.SubElement( + key = config.etree.SubElement( # type: ignore[attr-defined] pair, f"{self.ns}key", ) key.text = "normal" - pair.append(self.normal.etree_element()) # type: ignore[arg-type] + pair.append(self.normal.etree_element()) if self.highlight and isinstance(self.highlight, (Style, StyleUrl)): - pair = config.etree.SubElement( - element, # type: ignore[arg-type] + pair = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}Pair", ) - key = config.etree.SubElement( + key = config.etree.SubElement( # type: ignore[attr-defined] pair, f"{self.ns}key", ) key.text = "highlight" - pair.append(self.highlight.etree_element()) # type: ignore[arg-type] + pair.append(self.highlight.etree_element()) return element diff --git a/pyproject.toml b/pyproject.toml index 685d87c7..7ccb8487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ show_error_codes = true [[tool.mypy.overrides]] module = [ - "fastkml.kml", "fastkml.gx", - "fastkml.styles", + "fastkml.kml", "fastkml.tests.oldunit_test", "fastkml.tests.config_test"] ignore_errors = true From 0b95d5504f4adcc59e114d5ba4d682e0f62df405 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 20:13:35 +0100 Subject: [PATCH 131/144] apply monkeytype stubs to kml --- .pre-commit-config.yaml | 13 ++ fastkml/gx.py | 2 +- fastkml/kml.py | 466 ++++++++++++++++++++++------------------ 3 files changed, 269 insertions(+), 212 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 729e8742..58bbf13a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,19 @@ repos: - id: pretty-format-json - id: requirements-txt-fixer - id: trailing-whitespace + - repo: https://github.com/ikamensh/flynt/ + rev: "0.76" + hooks: + - id: flynt + - repo: https://github.com/MarcoGorelli/absolufy-imports + rev: v0.3.1 + hooks: + - id: absolufy-imports + - repo: https://github.com/hakancelikdev/unimport + rev: 0.12.1 + hooks: + - id: unimport + args: [--remove, --include-star-import, --ignore-init, --gitignore] - repo: https://github.com/psf/black rev: 22.8.0 hooks: diff --git a/fastkml/gx.py b/fastkml/gx.py index c37f37dc..86e42c6c 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 Christian Ledermann +# Copyright (C) 2012 - 2022 Christian Ledermann # # This library is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free diff --git a/fastkml/kml.py b/fastkml/kml.py index 32c16e38..422db66e 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -28,7 +28,10 @@ import urllib.parse as urlparse from datetime import date from datetime import datetime +from typing import Iterator +from typing import List from typing import Optional +from typing import Union # note that there are some ISO 8601 timeparsers at pypi # but in my tests all of them had some errors so we rely on the @@ -39,127 +42,18 @@ import fastkml.atom as atom import fastkml.config as config import fastkml.gx as gx - -from .base import _BaseObject -from .base import _XMLObject -from .geometry import Geometry -from .styles import Style -from .styles import StyleMap -from .styles import StyleUrl -from .styles import _StyleSelector +from fastkml.base import _BaseObject +from fastkml.base import _XMLObject +from fastkml.geometry import Geometry +from fastkml.styles import Style +from fastkml.styles import StyleMap +from fastkml.styles import StyleUrl +from fastkml.styles import _StyleSelector +from fastkml.types import Element logger = logging.getLogger(__name__) -class KML: - """represents a KML File""" - - _features = [] - ns = None - - def __init__(self, ns=None): - """The namespace (ns) may be empty ('') if the 'kml:' prefix is - undesired. Note that all child elements like Document or Placemark need - to be initialized with empty namespace as well in this case. - - """ - self._features = [] - - self.ns = config.KMLNS if ns is None else ns - - def from_string(self, xml_string): - """create a KML object from a xml string""" - try: - element = config.etree.fromstring( - xml_string, parser=config.etree.XMLParser(huge_tree=True, recover=True) - ) - except TypeError: - element = config.etree.XML(xml_string) - - if not element.tag.endswith("kml"): - raise TypeError - - ns = element.tag.rstrip("kml") - documents = element.findall(f"{ns}Document") - for document in documents: - feature = Document(ns) - feature.from_element(document) - self.append(feature) - folders = element.findall(f"{ns}Folder") - for folder in folders: - feature = Folder(ns) - feature.from_element(folder) - self.append(feature) - placemarks = element.findall(f"{ns}Placemark") - for placemark in placemarks: - feature = Placemark(ns) - feature.from_element(placemark) - self.append(feature) - groundoverlays = element.findall(f"{ns}GroundOverlay") - for groundoverlay in groundoverlays: - feature = GroundOverlay(ns) - feature.from_element(groundoverlay) - self.append(feature) - photo_overlays = element.findall(f"{ns}PhotoOverlay") - for photo_overlay in photo_overlays: - feature = PhotoOverlay(ns) - feature.from_element(photo_overlay) - 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 - # element, just without prefix. - if not self.ns: - root = config.etree.Element(f"{self.ns}kml") - root.set("xmlns", config.KMLNS[1:-1]) - else: - try: - root = config.etree.Element( - f"{self.ns}kml", nsmap={None: self.ns[1:-1]} - ) - except TypeError: - root = config.etree.Element(f"{self.ns}kml") - for feature in self.features(): - root.append(feature.etree_element()) - return root - - def to_string(self, prettyprint=False): - """Return the KML Object as serialized xml""" - try: - return config.etree.tostring( - self.etree_element(), - encoding="UTF-8", - pretty_print=prettyprint, - ).decode("UTF-8") - except TypeError: - return config.etree.tostring(self.etree_element(), encoding="UTF-8").decode( - "UTF-8" - ) - - def features(self): - """iterate over features""" - for feature in self._features: - if isinstance(feature, (Document, Folder, Placemark, _Overlay)): - - yield feature - else: - raise TypeError( - "Features must be instances of " - "(Document, Folder, Placemark, Overlay)" - ) - - def append(self, kmlobj): - """append a feature""" - - if isinstance(kmlobj, (Document, Folder, Placemark, _Overlay)): - self._features.append(kmlobj) - else: - raise TypeError( - "Features must be instances of (Document, Folder, Placemark, Overlay)" - ) - - class _Feature(_BaseObject): """ abstract element; do not create @@ -275,10 +169,10 @@ def __init__( target_id: Optional[str] = None, name: Optional[str] = None, description: Optional[str] = None, - styles=None, + styles: Optional[List[Style]] = None, style_url: Optional[str] = None, - extended_data=None, - ): + extended_data: None = None, + ) -> None: super().__init__(ns=ns, id=id, target_id=target_id) self.name = name self.description = description @@ -408,14 +302,14 @@ def author(self, name): else: raise TypeError - def append_style(self, style): + def append_style(self, style: Union[Style, StyleMap]) -> None: """append a style to the feature""" if isinstance(style, _StyleSelector): self._styles.append(style) else: raise TypeError - def styles(self): + def styles(self) -> Iterator[Union[Style, StyleMap]]: """iterate over the styles of this feature""" for style in self._styles: if isinstance(style, _StyleSelector): @@ -491,7 +385,7 @@ def phone_number(self, phone_number): else: raise ValueError - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self.name: name = config.etree.SubElement(element, f"{self.ns}name") @@ -543,7 +437,7 @@ def etree_element(self): phone_number.text = self._phone_number return element - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) name = element.find(f"{self.ns}name") if name is not None: @@ -844,7 +738,7 @@ def http_query(self, http_query): else: raise ValueError - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self._href: @@ -882,7 +776,7 @@ def etree_element(self): return element - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) href = element.find(f"{self.ns}href") @@ -946,10 +840,10 @@ def __init__( target_id: Optional[str] = None, name: Optional[str] = None, description: Optional[str] = None, - styles=None, + styles: Optional[List[Style]] = None, style_url: Optional[str] = None, - features=None, - ): + features: None = None, + ) -> None: super().__init__( ns=ns, id=id, @@ -961,7 +855,7 @@ def __init__( ) self._features = features or [] - def features(self): + def features(self) -> Iterator[_Feature]: """iterate over features""" for feature in self._features: if isinstance(feature, (Folder, Placemark, Document, _Overlay)): @@ -972,13 +866,13 @@ def features(self): "(Folder, Placemark, Document, Overlay)" ) - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() for feature in self.features(): element.append(feature.etree_element()) return element - def append(self, kmlobj): + def append(self, kmlobj: _Feature) -> None: """append a feature""" if isinstance(kmlobj, (Folder, Placemark, Document, _Overlay)): self._features.append(kmlobj) @@ -1025,10 +919,10 @@ def __init__( target_id: Optional[str] = None, name: Optional[str] = None, description: Optional[str] = None, - styles=None, + styles: None = None, style_url: Optional[str] = None, icon: Optional[Icon] = None, - ): + ) -> None: super().__init__( ns=ns, id=id, @@ -1079,7 +973,7 @@ def icon(self, value): else: raise ValueError - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self._color: color = config.etree.SubElement(element, f"{self.ns}color") @@ -1091,7 +985,7 @@ def etree_element(self): element.append(self._icon.etree_element()) return element - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) color = element.find(f"{self.ns}color") if color is not None: @@ -1595,7 +1489,9 @@ def rotation(self, value): else: raise ValueError - def lat_lon_box(self, north, south, east, west, rotation=0): + def lat_lon_box( + self, north: int, south: int, east: int, west: int, rotation: int = 0 + ) -> None: if -90 <= float(north) <= 90: self.north = north else: @@ -1617,7 +1513,7 @@ def lat_lon_box(self, north, south, east, west, rotation=0): else: raise ValueError - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self._altitude: altitude = config.etree.SubElement(element, f"{self.ns}altitude") @@ -1642,7 +1538,7 @@ def etree_element(self): rotation.text = self._rotation return element - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) altitude = element.find(f"{self.ns}altitude") if altitude is not None: @@ -1679,11 +1575,11 @@ class Document(_Container): __name__ = "Document" _schemata = None - def schemata(self): + def schemata(self) -> None: if self._schemata: yield from self._schemata - def append_schema(self, schema): + def append_schema(self, schema: "Schema") -> None: if self._schemata is None: self._schemata = [] if isinstance(schema, Schema): @@ -1692,7 +1588,7 @@ def append_schema(self, schema): s = Schema(schema) self._schemata.append(s) - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) documents = element.findall(f"{self.ns}Document") for document in documents: @@ -1715,14 +1611,14 @@ def from_element(self, element): s.from_element(schema) self.append_schema(s) - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self._schemata is not None: for schema in self._schemata: element.append(schema.etree_element()) return element - def get_style_by_url(self, style_url): + def get_style_by_url(self, style_url: str) -> Union[Style, StyleMap]: id = urlparse.urlparse(style_url).fragment for style in self.styles(): if style.id == id: @@ -1737,7 +1633,7 @@ class Folder(_Container): __name__ = "Folder" - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) folders = element.findall(f"{self.ns}Folder") for folder in folders: @@ -1778,7 +1674,7 @@ def geometry(self, geometry): else: self._geometry = Geometry(ns=self.ns, geometry=geometry) - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) point = element.find(f"{self.ns}Point") if point is not None: @@ -1826,7 +1722,7 @@ def from_element(self, element): logger.debug("Problem with element: %", config.etree.tostring(element)) # raise ValueError('No geometries found') - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self._geometry is not None: element.append(self._geometry.etree_element()) @@ -1853,7 +1749,11 @@ class _TimePrimitive(_BaseObject): RESOLUTIONS = ["gYear", "gYearMonth", "date", "dateTime"] - def get_resolution(self, dt, resolution=None): + def get_resolution( + self, + dt: Optional[Union[date, datetime]], + resolution: Optional[str] = None, + ) -> Optional[str]: if resolution: if resolution not in self.RESOLUTIONS: raise ValueError @@ -1867,7 +1767,7 @@ def get_resolution(self, dt, resolution=None): resolution = None return resolution - def parse_str(self, datestr): + def parse_str(self, datestr: str) -> List[Union[datetime, str]]: resolution = "dateTime" year = 0 month = 1 @@ -1896,7 +1796,11 @@ def parse_str(self, datestr): raise ValueError return [dt, resolution] - def date_to_string(self, dt, resolution=None): + def date_to_string( + self, + dt: Optional[Union[date, datetime]], + resolution: Optional[str] = None, + ) -> Optional[str]: if isinstance(dt, (date, datetime)): resolution = self.get_resolution(dt, resolution) if resolution == "gYear": @@ -1919,18 +1823,24 @@ class TimeStamp(_TimePrimitive): __name__ = "TimeStamp" timestamp = None - def __init__(self, ns=None, id=None, timestamp=None, resolution=None): + def __init__( + self, + ns: Optional[str] = None, + id: None = None, + timestamp: Optional[Union[date, datetime]] = None, + resolution: None = None, + ) -> None: super().__init__(ns, id) resolution = self.get_resolution(timestamp, resolution) self.timestamp = [timestamp, resolution] - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() when = config.etree.SubElement(element, f"{self.ns}when") when.text = self.date_to_string(*self.timestamp) return element - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) when = element.find(f"{self.ns}when") if when is not None: @@ -1945,8 +1855,14 @@ class TimeSpan(_TimePrimitive): end = None def __init__( - self, ns=None, id=None, begin=None, begin_res=None, end=None, end_res=None - ): + self, + ns: Optional[str] = None, + id: None = None, + begin: Optional[Union[date, datetime]] = None, + begin_res: None = None, + end: Optional[Union[date, datetime]] = None, + end_res: None = None, + ) -> None: super().__init__(ns, id) if begin: resolution = self.get_resolution(begin, begin_res) @@ -1955,7 +1871,7 @@ def __init__( resolution = self.get_resolution(end, end_res) self.end = [end, resolution] - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) begin = element.find(f"{self.ns}begin") if begin is not None: @@ -1964,7 +1880,7 @@ def from_element(self, element): if end is not None: self.end = self.parse_str(end.text) - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self.begin is not None: text = self.date_to_string(*self.begin) @@ -1998,7 +1914,13 @@ class Schema(_BaseObject): # omitted, the field is ignored. name = None - def __init__(self, ns=None, id=None, name=None, fields=None): + def __init__( + self, + ns: Optional[str] = None, + id: Optional[str] = None, + name: None = None, + fields: None = None, + ) -> None: if id is None: raise ValueError("Id is required for schema") super().__init__(ns, id) @@ -2033,7 +1955,7 @@ def simple_fields(self, fields): else: raise ValueError("Fields must be of type list, tuple or dict") - def append(self, type, name, display_name=None): + def append(self, type: str, name: str, display_name: Optional[str] = None) -> None: """ append a field. The declaration of the custom field, must specify both the type @@ -2076,7 +1998,7 @@ def append(self, type, name, display_name=None): {"type": type, "name": name, "displayName": display_name} ) - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) self.name = element.get("name") simple_fields = element.findall(f"{self.ns}SimpleField") @@ -2088,7 +2010,7 @@ def from_element(self, element): sfdisplay_name = display_name.text if display_name is not None else None self.append(sftype, sfname, sfdisplay_name) - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self.name: element.set("name", self.name) @@ -2102,6 +2024,45 @@ def etree_element(self): return element +class Data(_XMLObject): + """Represents an untyped name/value pair with optional display name.""" + + __name__ = "Data" + + def __init__( + self, + ns: Optional[str] = None, + name: Optional[str] = None, + value: Optional[str] = None, + display_name: Optional[str] = None, + ) -> None: + super().__init__(ns) + + self.name = name + self.value = value + self.display_name = display_name + + def etree_element(self) -> Element: + element = super().etree_element() + element.set("name", self.name) + value = config.etree.SubElement(element, f"{self.ns}value") + value.text = self.value + if self.display_name: + display_name = config.etree.SubElement(element, f"{self.ns}displayName") + display_name.text = self.display_name + return element + + def from_element(self, element: Element) -> None: + super().from_element(element) + self.name = element.get("name") + tmp_value = element.find(f"{self.ns}value") + if tmp_value is not None: + self.value = tmp_value.text + display_name = element.find(f"{self.ns}displayName") + if display_name is not None: + self.display_name = display_name.text + + class ExtendedData(_XMLObject): """Represents a list of untyped name/value pairs. See docs: @@ -2112,17 +2073,19 @@ class ExtendedData(_XMLObject): __name__ = "ExtendedData" - def __init__(self, ns=None, elements=None): + def __init__( + self, ns: Optional[str] = None, elements: Optional[List[Data]] = None + ) -> None: super().__init__(ns) self.elements = elements or [] - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() for subelement in self.elements: element.append(subelement.etree_element()) return element - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) self.elements = [] untyped_data = element.findall(f"{self.ns}Data") @@ -2137,39 +2100,6 @@ def from_element(self, element): self.elements.append(el) -class Data(_XMLObject): - """Represents an untyped name/value pair with optional display name.""" - - __name__ = "Data" - - def __init__(self, ns=None, name=None, value=None, display_name=None): - super().__init__(ns) - - self.name = name - self.value = value - self.display_name = display_name - - def etree_element(self): - element = super().etree_element() - element.set("name", self.name) - value = config.etree.SubElement(element, f"{self.ns}value") - value.text = self.value - if self.display_name: - display_name = config.etree.SubElement(element, f"{self.ns}displayName") - display_name.text = self.display_name - return element - - def from_element(self, element): - super().from_element(element) - self.name = element.get("name") - tmp_value = element.find(f"{self.ns}value") - if tmp_value is not None: - self.value = tmp_value.text - display_name = element.find(f"{self.ns}displayName") - if display_name is not None: - self.display_name = display_name.text - - class SchemaData(_XMLObject): """ @@ -2187,7 +2117,12 @@ class SchemaData(_XMLObject): schema_url = None _data = None - def __init__(self, ns=None, schema_url=None, data=None): + def __init__( + self, + ns: Optional[str] = None, + schema_url: Optional[str] = None, + data: None = None, + ) -> None: super().__init__(ns) if (not isinstance(schema_url, str)) or (not schema_url): raise ValueError("required parameter schema_url missing") @@ -2213,13 +2148,13 @@ def data(self, data): else: raise TypeError("data must be of type tuple or list") - def append_data(self, name, value): + def append_data(self, name: str, value: Union[int, str]) -> None: if isinstance(name, str) and name: self._data.append({"name": name, "value": value}) else: raise TypeError("name must be a nonempty string") - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() element.set("schemaUrl", self.schema_url) for data in self.data: @@ -2228,7 +2163,7 @@ def etree_element(self): sd.text = data["value"] return element - def from_element(self, element): + def from_element(self, element: Element) -> None: super().from_element(element) self.data = [] self.schema_url = element.get("schemaUrl") @@ -2382,16 +2317,16 @@ class Camera(_AbstractView): def __init__( self, - ns=None, - id=None, - longitude=None, - latitude=None, - altitude=None, - heading=None, - tilt=None, - roll=None, - altitude_mode="relativeToGround", - ): + ns: None = None, + id: None = None, + longitude: Optional[int] = None, + latitude: Optional[int] = None, + altitude: Optional[int] = None, + heading: Optional[int] = None, + tilt: Optional[int] = None, + roll: Optional[int] = None, + altitude_mode: str = "relativeToGround", + ) -> None: super().__init__(ns, id) self._longitude = longitude self._latitude = latitude @@ -2745,6 +2680,115 @@ def etree_element(self): return element +class KML: + """represents a KML File""" + + _features = [] + ns = None + + def __init__(self, ns: Optional[str] = None) -> None: + """The namespace (ns) may be empty ('') if the 'kml:' prefix is + undesired. Note that all child elements like Document or Placemark need + to be initialized with empty namespace as well in this case. + + """ + self._features = [] + + self.ns = config.KMLNS if ns is None else ns + + def from_string(self, xml_string: str) -> None: + """create a KML object from a xml string""" + try: + element = config.etree.fromstring( + xml_string, parser=config.etree.XMLParser(huge_tree=True, recover=True) + ) + except TypeError: + element = config.etree.XML(xml_string) + + if not element.tag.endswith("kml"): + raise TypeError + + ns = element.tag.rstrip("kml") + documents = element.findall(f"{ns}Document") + for document in documents: + feature = Document(ns) + feature.from_element(document) + self.append(feature) + folders = element.findall(f"{ns}Folder") + for folder in folders: + feature = Folder(ns) + feature.from_element(folder) + self.append(feature) + placemarks = element.findall(f"{ns}Placemark") + for placemark in placemarks: + feature = Placemark(ns) + feature.from_element(placemark) + self.append(feature) + groundoverlays = element.findall(f"{ns}GroundOverlay") + for groundoverlay in groundoverlays: + feature = GroundOverlay(ns) + feature.from_element(groundoverlay) + self.append(feature) + photo_overlays = element.findall(f"{ns}PhotoOverlay") + for photo_overlay in photo_overlays: + feature = PhotoOverlay(ns) + feature.from_element(photo_overlay) + self.append(feature) + + def etree_element(self) -> Element: + # self.ns may be empty, which leads to unprefixed kml elements. + # However, in this case the xlmns should still be mentioned on the kml + # element, just without prefix. + if not self.ns: + root = config.etree.Element(f"{self.ns}kml") + root.set("xmlns", config.KMLNS[1:-1]) + else: + try: + root = config.etree.Element( + f"{self.ns}kml", nsmap={None: self.ns[1:-1]} + ) + except TypeError: + root = config.etree.Element(f"{self.ns}kml") + for feature in self.features(): + root.append(feature.etree_element()) + return root + + def to_string(self, prettyprint: bool = False) -> str: + """Return the KML Object as serialized xml""" + try: + return config.etree.tostring( + self.etree_element(), + encoding="UTF-8", + pretty_print=prettyprint, + ).decode("UTF-8") + except TypeError: + return config.etree.tostring(self.etree_element(), encoding="UTF-8").decode( + "UTF-8" + ) + + def features(self) -> Iterator[Union[Folder, Document, Placemark]]: + """iterate over features""" + for feature in self._features: + if isinstance(feature, (Document, Folder, Placemark, _Overlay)): + + yield feature + else: + raise TypeError( + "Features must be instances of " + "(Document, Folder, Placemark, Overlay)" + ) + + def append(self, kmlobj: Union[Folder, Document, Placemark]) -> None: + """append a feature""" + + if isinstance(kmlobj, (Document, Folder, Placemark, _Overlay)): + self._features.append(kmlobj) + else: + raise TypeError( + "Features must be instances of (Document, Folder, Placemark, Overlay)" + ) + + __all__ = [ "Data", "Document", From 22f1ef0633a817b40b640a5e337cac26ff4332b7 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 20:16:21 +0100 Subject: [PATCH 132/144] enable typechecking for kml --- pyproject.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7ccb8487..9f6fd0f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ exclude = ["**/node_modules", "**/__pycache__", ".pytype", ".pyre", - "fastkml/test_main.py" ] [tool.mypy] @@ -40,7 +39,5 @@ show_error_codes = true # mypy per-module options: [[tool.mypy.overrides]] -module = [ - "fastkml.kml", - "fastkml.tests.oldunit_test", "fastkml.tests.config_test"] +module = ["fastkml.tests.oldunit_test", "fastkml.tests.config_test"] ignore_errors = true From 7184cff6a0081f904df034965606b432cda5709c Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 20:23:52 +0100 Subject: [PATCH 133/144] manual annotate where monkeytype failed --- fastkml/kml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 422db66e..432ae136 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -184,14 +184,14 @@ def __init__( self.extended_data = extended_data @property - def style_url(self): + def style_url(self) -> Optional[str]: """Returns the url only, not a full StyleUrl object. if you need the full StyleUrl object use _style_url""" if isinstance(self._style_url, StyleUrl): return self._style_url.url @style_url.setter - def style_url(self, styleurl): + def style_url(self, styleurl: Union[str, StyleUrl, None]) -> None: """you may pass a StyleUrl Object, a string or None""" if isinstance(styleurl, StyleUrl): self._style_url = styleurl From e90a63aa15aad82f034bc1b70f416558a1671fbc Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 8 Oct 2022 21:59:12 +0100 Subject: [PATCH 134/144] move data into its own modulw --- fastkml/__init__.py | 42 +++--- fastkml/data.py | 282 +++++++++++++++++++++++++++++++++++++ fastkml/kml.py | 334 +++++--------------------------------------- 3 files changed, 335 insertions(+), 323 deletions(-) create mode 100644 fastkml/data.py diff --git a/fastkml/__init__.py b/fastkml/__init__.py index 8c3916d8..d78d8ea1 100644 --- a/fastkml/__init__.py +++ b/fastkml/__init__.py @@ -27,27 +27,27 @@ from pkg_resources import DistributionNotFound from pkg_resources import get_distribution -from .atom import Author -from .atom import Contributor -from .atom import Link -from .kml import KML -from .kml import Data -from .kml import Document -from .kml import ExtendedData -from .kml import Folder -from .kml import Placemark -from .kml import Schema -from .kml import SchemaData -from .kml import TimeSpan -from .kml import TimeStamp -from .styles import BalloonStyle -from .styles import IconStyle -from .styles import LabelStyle -from .styles import LineStyle -from .styles import PolyStyle -from .styles import Style -from .styles import StyleMap -from .styles import StyleUrl +from fastkml.atom import Author +from fastkml.atom import Contributor +from fastkml.atom import Link +from fastkml.data import Data +from fastkml.data import ExtendedData +from fastkml.data import Schema +from fastkml.data import SchemaData +from fastkml.kml import KML +from fastkml.kml import Document +from fastkml.kml import Folder +from fastkml.kml import Placemark +from fastkml.kml import TimeSpan +from fastkml.kml import TimeStamp +from fastkml.styles import BalloonStyle +from fastkml.styles import IconStyle +from fastkml.styles import LabelStyle +from fastkml.styles import LineStyle +from fastkml.styles import PolyStyle +from fastkml.styles import Style +from fastkml.styles import StyleMap +from fastkml.styles import StyleUrl try: __version__ = get_distribution("fastkml").version diff --git a/fastkml/data.py b/fastkml/data.py new file mode 100644 index 00000000..f7b3e6b2 --- /dev/null +++ b/fastkml/data.py @@ -0,0 +1,282 @@ +from typing import List +from typing import Optional +from typing import Union + +import fastkml.config as config +from fastkml.base import _BaseObject +from fastkml.base import _XMLObject +from fastkml.types import Element + + +class Schema(_BaseObject): + """ + Specifies a custom KML schema that is used to add custom data to + KML Features. + The "id" attribute is required and must be unique within the KML file. + is always a child of . + """ + + __name__ = "Schema" + + _simple_fields = None + # The declaration of the custom fields, each of which must specify both the + # type and the name of this field. If either the type or the name is + # omitted, the field is ignored. + name = None + + def __init__( + self, + ns: Optional[str] = None, + id: Optional[str] = None, + name: None = None, + fields: None = None, + ) -> None: + if id is None: + raise ValueError("Id is required for schema") + super().__init__(ns, id) + self.simple_fields = fields + self.name = name + + @property + def simple_fields(self): + return tuple( + { + "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") + ) + + @simple_fields.setter + def simple_fields(self, fields): + self._simple_fields = [] + if isinstance(fields, dict): + self.append(**fields) + elif isinstance(fields, (list, tuple)): + for field in fields: + if isinstance(field, (list, tuple)): + self.append(*field) + elif isinstance(field, dict): + self.append(**field) + elif fields is None: + self._simple_fields = [] + else: + raise ValueError("Fields must be of type list, tuple or dict") + + def append(self, type: str, name: str, display_name: Optional[str] = None) -> None: + """ + append a field. + The declaration of the custom field, must specify both the type + and the name of this field. + If either the type or the name is omitted, the field is ignored. + + The type can be one of the following: + string + int + uint + short + ushort + float + double + bool + + + The name, if any, to be used when the field name is displayed to + the Google Earth user. Use the [CDATA] element to escape standard + HTML markup. + """ + allowed_types = [ + "string", + "int", + "uint", + "short", + "ushort", + "float", + "double", + "bool", + ] + if type not in allowed_types: + raise TypeError( + f"{name} has the type {type} which is invalid. " + "The type must be one of " + "'string', 'int', 'uint', 'short', " + "'ushort', 'float', 'double', 'bool'" + ) + self._simple_fields.append( + {"type": type, "name": name, "displayName": display_name} + ) + + def from_element(self, element: Element) -> None: + super().from_element(element) + self.name = element.get("name") + simple_fields = element.findall(f"{self.ns}SimpleField") + self.simple_fields = None + for simple_field in simple_fields: + sfname = simple_field.get("name") + sftype = simple_field.get("type") + display_name = simple_field.find(f"{self.ns}displayName") + sfdisplay_name = display_name.text if display_name is not None else None + self.append(sftype, sfname, sfdisplay_name) + + def etree_element(self) -> Element: + element = super().etree_element() + if self.name: + element.set("name", self.name) + for simple_field in self.simple_fields: + sf = config.etree.SubElement(element, f"{self.ns}SimpleField") + sf.set("type", simple_field["type"]) + sf.set("name", simple_field["name"]) + if simple_field.get("displayName"): + dn = config.etree.SubElement(sf, f"{self.ns}displayName") + dn.text = simple_field["displayName"] + return element + + +class Data(_XMLObject): + """Represents an untyped name/value pair with optional display name.""" + + __name__ = "Data" + + def __init__( + self, + ns: Optional[str] = None, + name: Optional[str] = None, + value: Optional[str] = None, + display_name: Optional[str] = None, + ) -> None: + super().__init__(ns) + + self.name = name + self.value = value + self.display_name = display_name + + def etree_element(self) -> Element: + element = super().etree_element() + element.set("name", self.name) + value = config.etree.SubElement(element, f"{self.ns}value") + value.text = self.value + if self.display_name: + display_name = config.etree.SubElement(element, f"{self.ns}displayName") + display_name.text = self.display_name + return element + + def from_element(self, element: Element) -> None: + super().from_element(element) + self.name = element.get("name") + tmp_value = element.find(f"{self.ns}value") + if tmp_value is not None: + self.value = tmp_value.text + display_name = element.find(f"{self.ns}displayName") + if display_name is not None: + self.display_name = display_name.text + + +class ExtendedData(_XMLObject): + """Represents a list of untyped name/value pairs. See docs: + + -> 'Adding Untyped Name/Value Pairs' + https://developers.google.com/kml/documentation/extendeddata + + """ + + __name__ = "ExtendedData" + + def __init__( + self, ns: Optional[str] = None, elements: Optional[List[Data]] = None + ) -> None: + super().__init__(ns) + self.elements = elements or [] + + def etree_element(self) -> Element: + element = super().etree_element() + for subelement in self.elements: + element.append(subelement.etree_element()) + return element + + def from_element(self, element: Element) -> None: + super().from_element(element) + self.elements = [] + untyped_data = element.findall(f"{self.ns}Data") + for ud in untyped_data: + el = Data(self.ns) + el.from_element(ud) + self.elements.append(el) + typed_data = element.findall(f"{self.ns}SchemaData") + for sd in typed_data: + el = SchemaData(self.ns, "dummy") + el.from_element(sd) + self.elements.append(el) + + +class SchemaData(_XMLObject): + """ + + This element is used in conjunction with to add typed + custom data to a KML Feature. The Schema element (identified by the + schemaUrl attribute) declares the custom data type. The actual data + objects ("instances" of the custom data) are defined using the + SchemaData element. + The can be a full URL, a reference to a Schema ID defined + in an external KML file, or a reference to a Schema ID defined + in the same KML file. + """ + + __name__ = "SchemaData" + schema_url = None + _data = None + + def __init__( + self, + ns: Optional[str] = None, + schema_url: Optional[str] = None, + data: None = None, + ) -> None: + super().__init__(ns) + if (not isinstance(schema_url, str)) or (not schema_url): + raise ValueError("required parameter schema_url missing") + self.schema_url = schema_url + self._data = [] + self.data = data + + @property + def data(self): + return tuple(self._data) + + @data.setter + def data(self, data): + if isinstance(data, (tuple, list)): + self._data = [] + for d in data: + if isinstance(d, (tuple, list)): + self.append_data(*d) + elif isinstance(d, dict): + self.append_data(**d) + elif data is None: + self._data = [] + else: + raise TypeError("data must be of type tuple or list") + + def append_data(self, name: str, value: Union[int, str]) -> None: + if isinstance(name, str) and name: + self._data.append({"name": name, "value": value}) + else: + raise TypeError("name must be a nonempty string") + + def etree_element(self) -> Element: + element = super().etree_element() + element.set("schemaUrl", self.schema_url) + for data in self.data: + sd = config.etree.SubElement(element, f"{self.ns}SimpleData") + sd.set("name", data["name"]) + sd.text = data["value"] + return element + + def from_element(self, element: Element) -> None: + super().from_element(element) + self.data = [] + self.schema_url = element.get("schemaUrl") + simple_data = element.findall(f"{self.ns}SimpleData") + for sd in simple_data: + self.append_data(sd.get("name"), sd.text) diff --git a/fastkml/kml.py b/fastkml/kml.py index 432ae136..4450345b 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -43,7 +43,10 @@ import fastkml.config as config import fastkml.gx as gx from fastkml.base import _BaseObject -from fastkml.base import _XMLObject +from fastkml.data import Data +from fastkml.data import ExtendedData +from fastkml.data import Schema +from fastkml.data import SchemaData from fastkml.geometry import Geometry from fastkml.styles import Style from fastkml.styles import StyleMap @@ -896,7 +899,7 @@ class _Overlay(_Feature): _color = None # Color values expressed in hexadecimal notation, including opacity (alpha) - # values. The order of expression is alpOverlayha, blue, green, red + # values. The order of expression is alphaOverlay, blue, green, red # (AABBGGRR). The range of values for any one color is 0 to 255 (00 to ff). # For opacity, 00 is fully transparent and ff is fully opaque. @@ -1898,280 +1901,6 @@ def etree_element(self) -> Element: return element -class Schema(_BaseObject): - """ - Specifies a custom KML schema that is used to add custom data to - KML Features. - The "id" attribute is required and must be unique within the KML file. - is always a child of . - """ - - __name__ = "Schema" - - _simple_fields = None - # The declaration of the custom fields, each of which must specify both the - # type and the name of this field. If either the type or the name is - # omitted, the field is ignored. - name = None - - def __init__( - self, - ns: Optional[str] = None, - id: Optional[str] = None, - name: None = None, - fields: None = None, - ) -> None: - if id is None: - raise ValueError("Id is required for schema") - super().__init__(ns, id) - self.simple_fields = fields - self.name = name - - @property - def simple_fields(self): - return tuple( - { - "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") - ) - - @simple_fields.setter - def simple_fields(self, fields): - self._simple_fields = [] - if isinstance(fields, dict): - self.append(**fields) - elif isinstance(fields, (list, tuple)): - for field in fields: - if isinstance(field, (list, tuple)): - self.append(*field) - elif isinstance(field, dict): - self.append(**field) - elif fields is None: - self._simple_fields = [] - else: - raise ValueError("Fields must be of type list, tuple or dict") - - def append(self, type: str, name: str, display_name: Optional[str] = None) -> None: - """ - append a field. - The declaration of the custom field, must specify both the type - and the name of this field. - If either the type or the name is omitted, the field is ignored. - - The type can be one of the following: - string - int - uint - short - ushort - float - double - bool - - - The name, if any, to be used when the field name is displayed to - the Google Earth user. Use the [CDATA] element to escape standard - HTML markup. - """ - allowed_types = [ - "string", - "int", - "uint", - "short", - "ushort", - "float", - "double", - "bool", - ] - if type not in allowed_types: - raise TypeError( - f"{name} has the type {type} which is invalid. " - + "The type must be one of " - + "'string', 'int', 'uint', 'short', " - + "'ushort', 'float', 'double', 'bool'" - ) - self._simple_fields.append( - {"type": type, "name": name, "displayName": display_name} - ) - - def from_element(self, element: Element) -> None: - super().from_element(element) - self.name = element.get("name") - simple_fields = element.findall(f"{self.ns}SimpleField") - self.simple_fields = None - for simple_field in simple_fields: - sfname = simple_field.get("name") - sftype = simple_field.get("type") - display_name = simple_field.find(f"{self.ns}displayName") - sfdisplay_name = display_name.text if display_name is not None else None - self.append(sftype, sfname, sfdisplay_name) - - def etree_element(self) -> Element: - element = super().etree_element() - if self.name: - element.set("name", self.name) - for simple_field in self.simple_fields: - sf = config.etree.SubElement(element, f"{self.ns}SimpleField") - sf.set("type", simple_field["type"]) - sf.set("name", simple_field["name"]) - if simple_field.get("displayName"): - dn = config.etree.SubElement(sf, f"{self.ns}displayName") - dn.text = simple_field["displayName"] - return element - - -class Data(_XMLObject): - """Represents an untyped name/value pair with optional display name.""" - - __name__ = "Data" - - def __init__( - self, - ns: Optional[str] = None, - name: Optional[str] = None, - value: Optional[str] = None, - display_name: Optional[str] = None, - ) -> None: - super().__init__(ns) - - self.name = name - self.value = value - self.display_name = display_name - - def etree_element(self) -> Element: - element = super().etree_element() - element.set("name", self.name) - value = config.etree.SubElement(element, f"{self.ns}value") - value.text = self.value - if self.display_name: - display_name = config.etree.SubElement(element, f"{self.ns}displayName") - display_name.text = self.display_name - return element - - def from_element(self, element: Element) -> None: - super().from_element(element) - self.name = element.get("name") - tmp_value = element.find(f"{self.ns}value") - if tmp_value is not None: - self.value = tmp_value.text - display_name = element.find(f"{self.ns}displayName") - if display_name is not None: - self.display_name = display_name.text - - -class ExtendedData(_XMLObject): - """Represents a list of untyped name/value pairs. See docs: - - -> 'Adding Untyped Name/Value Pairs' - https://developers.google.com/kml/documentation/extendeddata - - """ - - __name__ = "ExtendedData" - - def __init__( - self, ns: Optional[str] = None, elements: Optional[List[Data]] = None - ) -> None: - super().__init__(ns) - self.elements = elements or [] - - def etree_element(self) -> Element: - element = super().etree_element() - for subelement in self.elements: - element.append(subelement.etree_element()) - return element - - def from_element(self, element: Element) -> None: - super().from_element(element) - self.elements = [] - untyped_data = element.findall(f"{self.ns}Data") - for ud in untyped_data: - el = Data(self.ns) - el.from_element(ud) - self.elements.append(el) - typed_data = element.findall(f"{self.ns}SchemaData") - for sd in typed_data: - el = SchemaData(self.ns, "dummy") - el.from_element(sd) - self.elements.append(el) - - -class SchemaData(_XMLObject): - """ - - This element is used in conjunction with to add typed - custom data to a KML Feature. The Schema element (identified by the - schemaUrl attribute) declares the custom data type. The actual data - objects ("instances" of the custom data) are defined using the - SchemaData element. - The can be a full URL, a reference to a Schema ID defined - in an external KML file, or a reference to a Schema ID defined - in the same KML file. - """ - - __name__ = "SchemaData" - schema_url = None - _data = None - - def __init__( - self, - ns: Optional[str] = None, - schema_url: Optional[str] = None, - data: None = None, - ) -> None: - super().__init__(ns) - if (not isinstance(schema_url, str)) or (not schema_url): - raise ValueError("required parameter schema_url missing") - self.schema_url = schema_url - self._data = [] - self.data = data - - @property - def data(self): - return tuple(self._data) - - @data.setter - def data(self, data): - if isinstance(data, (tuple, list)): - self._data = [] - for d in data: - if isinstance(d, (tuple, list)): - self.append_data(*d) - elif isinstance(d, dict): - self.append_data(**d) - elif data is None: - self._data = [] - else: - raise TypeError("data must be of type tuple or list") - - def append_data(self, name: str, value: Union[int, str]) -> None: - if isinstance(name, str) and name: - self._data.append({"name": name, "value": value}) - else: - raise TypeError("name must be a nonempty string") - - def etree_element(self) -> Element: - element = super().etree_element() - element.set("schemaUrl", self.schema_url) - for data in self.data: - sd = config.etree.SubElement(element, f"{self.ns}SimpleData") - sd.set("name", data["name"]) - sd.text = data["value"] - return element - - def from_element(self, element: Element) -> None: - super().from_element(element) - self.data = [] - self.schema_url = element.get("schemaUrl") - simple_data = element.findall(f"{self.ns}SimpleData") - for sd in simple_data: - self.append_data(sd.get("name"), sd.text) - - class _AbstractView(_BaseObject): """ This is an abstract element and cannot be used directly in a KML file. @@ -2207,7 +1936,7 @@ def begin(self): return self._gx_timespan.begin[0] @begin.setter - def begin(self, dt): + def begin(self, dt) -> None: if self._gx_timespan is None: self._gx_timespan = TimeSpan(begin=dt) elif self._gx_timespan.begin is None: @@ -2317,17 +2046,18 @@ class Camera(_AbstractView): def __init__( self, - ns: None = None, - id: None = None, - longitude: Optional[int] = None, - latitude: Optional[int] = None, - altitude: Optional[int] = None, - heading: Optional[int] = None, - tilt: Optional[int] = None, - roll: Optional[int] = None, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + longitude: Optional[float] = None, + latitude: Optional[float] = None, + altitude: Optional[float] = None, + heading: Optional[float] = None, + tilt: Optional[float] = None, + roll: Optional[float] = None, altitude_mode: str = "relativeToGround", ) -> None: - super().__init__(ns, id) + super().__init__(ns=ns, id=id, target_id=target_id) self._longitude = longitude self._latitude = latitude self._altitude = altitude @@ -2337,11 +2067,11 @@ def __init__( self._altitude_mode = altitude_mode @property - def longitude(self): + def longitude(self) -> Optional[float]: return self._longitude @longitude.setter - def longitude(self, value): + def longitude(self, value) -> None: if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): self._longitude = str(value) elif value is None: @@ -2350,11 +2080,11 @@ def longitude(self, value): raise ValueError @property - def latitude(self): + def latitude(self) -> Optional[float]: return self._latitude @latitude.setter - def latitude(self, value): + def latitude(self, value) -> None: if isinstance(value, (str, int, float)) and (-90 <= float(value) <= 90): self._latitude = str(value) elif value is None: @@ -2363,11 +2093,11 @@ def latitude(self, value): raise ValueError @property - def altitude(self): + def altitude(self) -> Optional[float]: return self._altitude @altitude.setter - def altitude(self, value): + def altitude(self, value) -> None: if isinstance(value, (str, int, float)): self._altitude = str(value) elif value is None: @@ -2376,11 +2106,11 @@ def altitude(self, value): raise ValueError @property - def heading(self): + def heading(self) -> Optional[float]: return self._heading @heading.setter - def heading(self, value): + def heading(self, value) -> None: if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): self._heading = str(value) elif value is None: @@ -2389,11 +2119,11 @@ def heading(self, value): raise ValueError @property - def tilt(self): + def tilt(self) -> Optional[float]: return self._tilt @tilt.setter - def tilt(self, value): + def tilt(self, value) -> None: if isinstance(value, (str, int, float)) and (0 <= float(value) <= 180): self._tilt = str(value) elif value is None: @@ -2402,11 +2132,11 @@ def tilt(self, value): raise ValueError @property - def roll(self): + def roll(self) -> Optional[float]: return self._roll @roll.setter - def roll(self, value): + def roll(self, value) -> None: if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): self._roll = str(value) elif value is None: @@ -2415,11 +2145,11 @@ def roll(self, value): raise ValueError @property - def altitude_mode(self): + def altitude_mode(self) -> str: return self._altitude_mode @altitude_mode.setter - def altitude_mode(self, mode): + def altitude_mode(self, mode) -> None: if mode in ("relativeToGround", "clampToGround", "absolute"): self._altitude_mode = str(mode) else: @@ -2428,7 +2158,7 @@ def altitude_mode(self, mode): # "altitude_mode must be one of " "relativeToGround, # clampToGround, absolute") - def from_element(self, element): + def from_element(self, element) -> None: super().from_element(element) longitude = element.find(f"{self.ns}longitude") if longitude is not None: @@ -2455,7 +2185,7 @@ def from_element(self, element): altitude_mode = element.find(f"{gx.NS}altitudeMode") self.altitude_mode = altitude_mode.text - def etree_element(self): + def etree_element(self) -> Element: element = super().etree_element() if self.longitude: longitude = config.etree.SubElement(element, f"{self.ns}longitude") From b16e191d6a6c2c29593c123773672ad9ce3a091d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 9 Oct 2022 10:54:49 +0100 Subject: [PATCH 135/144] split kml into smaller modules --- fastkml/data.py | 16 +++++++++++++--- fastkml/overlays.py | 0 fastkml/times.py | 0 fastkml/views.py | 0 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 fastkml/overlays.py create mode 100644 fastkml/times.py create mode 100644 fastkml/views.py diff --git a/fastkml/data.py b/fastkml/data.py index f7b3e6b2..043dea59 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -1,13 +1,22 @@ from typing import List from typing import Optional +from typing import Tuple from typing import Union +from typing_extensions import TypedDict + import fastkml.config as config from fastkml.base import _BaseObject from fastkml.base import _XMLObject from fastkml.types import Element +class SimpleField(TypedDict): + name: str + type: str + displayName: str + + class Schema(_BaseObject): """ Specifies a custom KML schema that is used to add custom data to @@ -28,17 +37,18 @@ def __init__( self, ns: Optional[str] = None, id: Optional[str] = None, - name: None = None, + target_id: Optional[str] = None, + name: Optional[str] = None, fields: None = None, ) -> None: if id is None: raise ValueError("Id is required for schema") - super().__init__(ns, id) + super().__init__(ns=ns, id=id, target_id=target_id) self.simple_fields = fields self.name = name @property - def simple_fields(self): + def simple_fields(self) -> Tuple[SimpleField, ...]: return tuple( { "type": simple_field["type"], diff --git a/fastkml/overlays.py b/fastkml/overlays.py new file mode 100644 index 00000000..e69de29b diff --git a/fastkml/times.py b/fastkml/times.py new file mode 100644 index 00000000..e69de29b diff --git a/fastkml/views.py b/fastkml/views.py new file mode 100644 index 00000000..e69de29b From 27a87478a9dba060b16c0d0c6e35d9e49a0aef84 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 9 Oct 2022 12:09:25 +0100 Subject: [PATCH 136/144] Overlays stay in kml.py --- fastkml/kml.py | 688 +------------------------------------------- fastkml/overlays.py | 0 fastkml/times.py | 183 ++++++++++++ fastkml/views.py | 520 +++++++++++++++++++++++++++++++++ 4 files changed, 707 insertions(+), 684 deletions(-) delete mode 100644 fastkml/overlays.py diff --git a/fastkml/kml.py b/fastkml/kml.py index 4450345b..169d92fb 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -26,19 +26,11 @@ """ import logging import urllib.parse as urlparse -from datetime import date -from datetime import datetime from typing import Iterator from typing import List from typing import Optional from typing import Union -# note that there are some ISO 8601 timeparsers at pypi -# but in my tests all of them had some errors so we rely on the -# tried and tested dateutil here which is more stable. As a side effect -# we can also parse non ISO compliant dateTimes -import dateutil.parser - import fastkml.atom as atom import fastkml.config as config import fastkml.gx as gx @@ -52,7 +44,11 @@ from fastkml.styles import StyleMap from fastkml.styles import StyleUrl from fastkml.styles import _StyleSelector +from fastkml.times import TimeSpan +from fastkml.times import TimeStamp from fastkml.types import Element +from fastkml.views import Camera +from fastkml.views import LookAt logger = logging.getLogger(__name__) @@ -1734,682 +1730,6 @@ def etree_element(self) -> Element: return element -class _TimePrimitive(_BaseObject): - """The dateTime is defined according to XML Schema time. - The value can be expressed as yyyy-mm-ddThh:mm:sszzzzzz, where T is - the separator between the date and the time, and the time zone is - either Z (for UTC) or zzzzzz, which represents ±hh:mm in relation to - UTC. Additionally, the value can be expressed as a date only. - - The precision of the dateTime is dictated by the dateTime value - which can be one of the following: - - - dateTime gives second resolution - - date gives day resolution - - gYearMonth gives month resolution - - gYear gives year resolution - """ - - RESOLUTIONS = ["gYear", "gYearMonth", "date", "dateTime"] - - def get_resolution( - self, - dt: Optional[Union[date, datetime]], - resolution: Optional[str] = None, - ) -> Optional[str]: - if resolution: - if resolution not in self.RESOLUTIONS: - raise ValueError - else: - return resolution - elif isinstance(dt, datetime): - resolution = "dateTime" - elif isinstance(dt, date): - resolution = "date" - else: - resolution = None - return resolution - - def parse_str(self, datestr: str) -> List[Union[datetime, str]]: - resolution = "dateTime" - year = 0 - month = 1 - day = 1 - if len(datestr) == 4: - resolution = "gYear" - year = int(datestr) - dt = datetime(year, month, day) - elif len(datestr) == 6: - resolution = "gYearMonth" - year = int(datestr[:4]) - month = int(datestr[-2:]) - dt = datetime(year, month, day) - elif len(datestr) == 7: - resolution = "gYearMonth" - year = int(datestr.split("-")[0]) - month = int(datestr.split("-")[1]) - dt = datetime(year, month, day) - elif len(datestr) in [8, 10]: - resolution = "date" - dt = dateutil.parser.parse(datestr) - elif len(datestr) > 10: - resolution = "dateTime" - dt = dateutil.parser.parse(datestr) - else: - raise ValueError - return [dt, resolution] - - def date_to_string( - self, - dt: Optional[Union[date, datetime]], - resolution: Optional[str] = None, - ) -> Optional[str]: - if isinstance(dt, (date, datetime)): - resolution = self.get_resolution(dt, resolution) - if resolution == "gYear": - return dt.strftime("%Y") - elif resolution == "gYearMonth": - return dt.strftime("%Y-%m") - elif resolution == "date": - return ( - dt.date().isoformat() - if isinstance(dt, datetime) - else dt.isoformat() - ) - elif resolution == "dateTime": - return dt.isoformat() - - -class TimeStamp(_TimePrimitive): - """Represents a single moment in time.""" - - __name__ = "TimeStamp" - timestamp = None - - def __init__( - self, - ns: Optional[str] = None, - id: None = None, - timestamp: Optional[Union[date, datetime]] = None, - resolution: None = None, - ) -> None: - super().__init__(ns, id) - resolution = self.get_resolution(timestamp, resolution) - self.timestamp = [timestamp, resolution] - - def etree_element(self) -> Element: - element = super().etree_element() - when = config.etree.SubElement(element, f"{self.ns}when") - when.text = self.date_to_string(*self.timestamp) - return element - - def from_element(self, element: Element) -> None: - super().from_element(element) - when = element.find(f"{self.ns}when") - if when is not None: - self.timestamp = self.parse_str(when.text) - - -class TimeSpan(_TimePrimitive): - """Represents an extent in time bounded by begin and end dateTimes.""" - - __name__ = "TimeSpan" - begin = None - end = None - - def __init__( - self, - ns: Optional[str] = None, - id: None = None, - begin: Optional[Union[date, datetime]] = None, - begin_res: None = None, - end: Optional[Union[date, datetime]] = None, - end_res: None = None, - ) -> None: - super().__init__(ns, id) - if begin: - resolution = self.get_resolution(begin, begin_res) - self.begin = [begin, resolution] - if end: - resolution = self.get_resolution(end, end_res) - self.end = [end, resolution] - - def from_element(self, element: Element) -> None: - super().from_element(element) - begin = element.find(f"{self.ns}begin") - if begin is not None: - self.begin = self.parse_str(begin.text) - end = element.find(f"{self.ns}end") - if end is not None: - self.end = self.parse_str(end.text) - - def etree_element(self) -> Element: - element = super().etree_element() - if self.begin is not None: - text = self.date_to_string(*self.begin) - if text: - begin = config.etree.SubElement(element, f"{self.ns}begin") - begin.text = text - if self.end is not None: - text = self.date_to_string(*self.end) - if text: - end = config.etree.SubElement(element, f"{self.ns}end") - end.text = text - if self.begin == self.end is None: - raise ValueError("Either begin, end or both must be set") - # TODO test if end > begin - return element - - -class _AbstractView(_BaseObject): - """ - This is an abstract element and cannot be used directly in a KML file. - This element is extended by the and elements. - """ - - _gx_timespan = None - _gx_timestamp = None - - def etree_element(self): - element = super().etree_element() - if (self._timespan is not None) and (self._timestamp is not None): - raise ValueError("Either Timestamp or Timespan can be defined, not both") - if self._timespan is not None: - element.append(self._gx_timespan.etree_element()) - elif self._timestamp is not None: - element.append(self._gx_timestamp.etree_element()) - return element - - @property - def gx_timestamp(self): - return self._gx_timestamp - - @gx_timestamp.setter - def gx_timestamp(self, dt): - self._gx_timestamp = None if dt is None else TimeStamp(timestamp=dt) - if self._gx_timestamp is not None: - logger.warning("Setting a TimeStamp, TimeSpan deleted") - self._gx_timespan = None - - @property - def begin(self): - return self._gx_timespan.begin[0] - - @begin.setter - def begin(self, dt) -> None: - if self._gx_timespan is None: - self._gx_timespan = TimeSpan(begin=dt) - elif self._gx_timespan.begin is None: - self._gx_timespan.begin = [dt, None] - else: - self._gx_timespan.begin[0] = dt - if self._gx_timestamp is not None: - logger.warning("Setting a TimeSpan, TimeStamp deleted") - self._gx_timestamp = None - - @property - def end(self): - return self._gx_timespan.end[0] - - @end.setter - def end(self, dt): - if self._gx_timespan is None: - self._gx_timespan = TimeSpan(end=dt) - elif self._gx_timespan.end is None: - self._gx_timespan.end = [dt, None] - else: - self._gx_timespan.end[0] = dt - if self._gx_timestamp is not None: - logger.warning("Setting a TimeSpan, TimeStamp deleted") - self._gx_timestamp = None - - def from_element(self, element): - super().from_element(element) - gx_timespan = element.find(f"{gx.NS}TimeSpan") - if gx_timespan is not None: - self._gx_timespan = gx_timespan.text - gx_timestamp = element.find(f"{gx.NS}TimeStamp") - if gx_timestamp is not None: - self._gx_timestamp = gx_timestamp.text - - # TODO: - # TODO: - - -class Camera(_AbstractView): - """ - Defines the virtual camera that views the scene. This element defines - the position of the camera relative to the Earth's surface as well - as the viewing direction of the camera. The camera position is defined - by , , , and either or - . The viewing direction of the camera is defined by - , , and . can be a child element of any - Feature or of . A parent element cannot contain both a - and a at the same time. - - provides full six-degrees-of-freedom control over the view, - so you can position the Camera in space and then rotate it around the - X, Y, and Z axes. Most importantly, you can tilt the camera view so that - you're looking above the horizon into the sky. - - can also contain a TimePrimitive ( or ). - Time values in Camera affect historical imagery, sunlight, and the display of - time-stamped features. For more information, read Time with AbstractViews in - the Time and Animation chapter of the Developer's Guide. - """ - - __name__ = "Camera" - - _longitude = None - # Longitude of the virtual camera (eye point). Angular distance in degrees, - # relative to the Prime Meridian. Values west of the Meridian range from - # −180 to 0 degrees. Values east of the Meridian range from 0 to 180 degrees. - - _latitude = None - # Latitude of the virtual camera. Degrees north or south of the Equator - # (0 degrees). Values range from −90 degrees to 90 degrees. - - _altitude = None - # Distance of the camera from the earth's surface, in meters. Interpreted - # according to the Camera's or . - - _heading = None - # Direction (azimuth) of the camera, in degrees. Default=0 (true North). - # (See diagram.) Values range from 0 to 360 degrees. - - _tilt = None - # Rotation, in degrees, of the camera around the X axis. A value of 0 - # indicates that the view is aimed straight down toward the earth (the - # most common case). A value for 90 for indicates that the view - # is aimed toward the horizon. Values greater than 90 indicate that the - # view is pointed up into the sky. Values for are clamped at +180 - # degrees. - - _roll = None - # Rotation, in degrees, of the camera around the Z axis. Values range from - # −180 to +180 degrees. - - _altitude_mode = "relativeToGround" - # Specifies how the specified for the Camera is interpreted. - # Possible values are as follows: - # relativeToGround - - # (default) Interprets the as a value in meters above the - # ground. If the point is over water, the will be - # interpreted as a value in meters above sea level. See - # below to specify points relative to the sea floor. - # clampToGround - - # For a camera, this setting also places the camera relativeToGround, - # since putting the camera exactly at terrain height would mean that - # the eye would intersect the terrain (and the view would be blocked). - # absolute - - # Interprets the as a value in meters above sea level. - - def __init__( - self, - ns: Optional[str] = None, - id: Optional[str] = None, - target_id: Optional[str] = None, - longitude: Optional[float] = None, - latitude: Optional[float] = None, - altitude: Optional[float] = None, - heading: Optional[float] = None, - tilt: Optional[float] = None, - roll: Optional[float] = None, - altitude_mode: str = "relativeToGround", - ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) - self._longitude = longitude - self._latitude = latitude - self._altitude = altitude - self._heading = heading - self._tilt = tilt - self._roll = roll - self._altitude_mode = altitude_mode - - @property - def longitude(self) -> Optional[float]: - return self._longitude - - @longitude.setter - def longitude(self, value) -> None: - if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): - self._longitude = str(value) - elif value is None: - self._longitude = None - else: - raise ValueError - - @property - def latitude(self) -> Optional[float]: - return self._latitude - - @latitude.setter - def latitude(self, value) -> None: - if isinstance(value, (str, int, float)) and (-90 <= float(value) <= 90): - self._latitude = str(value) - elif value is None: - self._latitude = None - else: - raise ValueError - - @property - def altitude(self) -> Optional[float]: - return self._altitude - - @altitude.setter - def altitude(self, value) -> None: - if isinstance(value, (str, int, float)): - self._altitude = str(value) - elif value is None: - self._altitude = None - else: - raise ValueError - - @property - def heading(self) -> Optional[float]: - return self._heading - - @heading.setter - def heading(self, value) -> None: - if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): - self._heading = str(value) - elif value is None: - self._heading = None - else: - raise ValueError - - @property - def tilt(self) -> Optional[float]: - return self._tilt - - @tilt.setter - def tilt(self, value) -> None: - if isinstance(value, (str, int, float)) and (0 <= float(value) <= 180): - self._tilt = str(value) - elif value is None: - self._tilt = None - else: - raise ValueError - - @property - def roll(self) -> Optional[float]: - return self._roll - - @roll.setter - def roll(self, value) -> None: - if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): - self._roll = str(value) - elif value is None: - self._roll = None - else: - raise ValueError - - @property - def altitude_mode(self) -> str: - return self._altitude_mode - - @altitude_mode.setter - def altitude_mode(self, mode) -> None: - if mode in ("relativeToGround", "clampToGround", "absolute"): - self._altitude_mode = str(mode) - else: - self._altitude_mode = "relativeToGround" - # raise ValueError( - # "altitude_mode must be one of " "relativeToGround, - # clampToGround, absolute") - - def from_element(self, element) -> None: - super().from_element(element) - longitude = element.find(f"{self.ns}longitude") - if longitude is not None: - self.longitude = longitude.text - latitude = element.find(f"{self.ns}latitude") - if latitude is not None: - self.latitude = latitude.text - altitude = element.find(f"{self.ns}altitude") - if altitude is not None: - self.altitude = altitude.text - heading = element.find(f"{self.ns}heading") - if heading is not None: - self.heading = heading.text - tilt = element.find(f"{self.ns}tilt") - if tilt is not None: - self.tilt = tilt.text - roll = element.find(f"{self.ns}roll") - if roll is not None: - self.roll = roll.text - altitude_mode = element.find(f"{self.ns}altitudeMode") - if altitude_mode is not None: - self.altitude_mode = altitude_mode.text - else: - altitude_mode = element.find(f"{gx.NS}altitudeMode") - self.altitude_mode = altitude_mode.text - - def etree_element(self) -> Element: - element = super().etree_element() - if self.longitude: - longitude = config.etree.SubElement(element, f"{self.ns}longitude") - longitude.text = self.longitude - if self.latitude: - latitude = config.etree.SubElement(element, f"{self.ns}latitude") - latitude.text = self.latitude - if self.altitude: - altitude = config.etree.SubElement(element, f"{self.ns}altitude") - altitude.text = self.altitude - if self.heading: - heading = config.etree.SubElement(element, f"{self.ns}heading") - heading.text = self.heading - if self.tilt: - tilt = config.etree.SubElement(element, f"{self.ns}tilt") - tilt.text = self.tilt - if self.roll: - roll = config.etree.SubElement(element, f"{self.ns}roll") - roll.text = self.roll - if self.altitude_mode in ("clampedToGround", "relativeToGround", "absolute"): - altitude_mode = config.etree.SubElement(element, f"{self.ns}altitudeMode") - elif self.altitude_mode in ("clampedToSeaFloor", "relativeToSeaFloor"): - altitude_mode = config.etree.SubElement(element, f"{gx.NS}altitudeMode") - altitude_mode.text = self.altitude_mode - return element - - -class LookAt(_AbstractView): - - _longitude = None - # Longitude of the point the camera is looking at. Angular distance in - # degrees, relative to the Prime Meridian. Values west of the Meridian - # range from −180 to 0 degrees. Values east of the Meridian range from - # 0 to 180 degrees. - - _latitude = None - # Latitude of the point the camera is looking at. Degrees north or south - # of the Equator (0 degrees). Values range from −90 degrees to 90 degrees. - - _altitude = None - # Distance from the earth's surface, in meters. Interpreted according to - # the LookAt's altitude mode. - - _heading = None - # Direction (that is, North, South, East, West), in degrees. Default=0 - # (North). (See diagram below.) Values range from 0 to 360 degrees. - - _tilt = None - # Angle between the direction of the LookAt position and the normal to the - # surface of the earth. (See diagram below.) Values range from 0 to 90 - # degrees. Values for cannot be negative. A value of 0 - # degrees indicates viewing from directly above. A value of 90 - # degrees indicates viewing along the horizon. - - _range = None - # Distance in meters from the point specified by , , - # and to the LookAt position. (See diagram below.) - - _altitude_mode = None - # Specifies how the specified for the LookAt point is - # interpreted. Possible values are as follows: - # clampToGround - - # (default) Indicates to ignore the specification and - # place the LookAt position on the ground. - # relativeToGround - - # Interprets the as a value in meters above the ground. - # absolute - - # Interprets the as a value in meters above sea level. - - @property - def longitude(self): - return self._longitude - - @longitude.setter - def longitude(self, value): - if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): - self._longitude = str(value) - elif value is None: - self._longitude = None - else: - raise ValueError - - @property - def latitude(self): - return self._latitude - - @latitude.setter - def latitude(self, value): - if isinstance(value, (str, int, float)) and (-90 <= float(value) <= 90): - self._latitude = str(value) - elif value is None: - self._latitude = None - else: - raise ValueError - - @property - def altitude(self): - return self._altitude - - @altitude.setter - def altitude(self, value): - if isinstance(value, (str, int, float)): - self._altitude = str(value) - elif value is None: - self._altitude = None - else: - raise ValueError - - @property - def heading(self): - return self._heading - - @heading.setter - def heading(self, value): - if isinstance(value, (str, int, float)): - self._heading = str(value) - elif value is None: - self._heading = None - else: - raise ValueError - - @property - def tilt(self): - return self._tilt - - @tilt.setter - def tilt(self, value): - if isinstance(value, (str, int, float)) and (0 <= float(value) <= 90): - self._tilt = str(value) - elif value is None: - self._tilt = None - else: - raise ValueError - - @property - def range(self): - return self._range - - @range.setter - def range(self, value): - if isinstance(value, (str, int, float)): - self._range = str(value) - elif value is None: - self._range = None - else: - raise ValueError - - @property - def altitude_mode(self): - return self._altitude_mode - - @altitude_mode.setter - def altitude_mode(self, mode): - if mode in ( - "relativeToGround", - "clampToGround", - "absolute", - "relativeToSeaFloor", - "clampToSeaFloor", - ): - self._altitude_mode = str(mode) - else: - self._altitude_mode = "relativeToGround" - # raise ValueError( - # "altitude_mode must be one of " - # + "relativeToGround, clampToGround, absolute, - # + relativeToSeaFloor, clampToSeaFloor" - # ) - - def from_element(self, element): - super().from_element(element) - longitude = element.find(f"{self.ns}longitude") - if longitude is not None: - self.longitude = longitude.text - latitude = element.find(f"{self.ns}latitude") - if latitude is not None: - self.latitude = latitude.text - altitude = element.find(f"{self.ns}altitude") - if altitude is not None: - self.altitude = altitude.text - heading = element.find(f"{self.ns}heading") - if heading is not None: - self.heading = heading.text - tilt = element.find(f"{self.ns}tilt") - if tilt is not None: - self.tilt = tilt.text - range_var = element.find(f"{self.ns}range") - if range_var is not None: - self.range = range_var.text - altitude_mode = element.find(f"{self.ns}altitudeMode") - if altitude_mode is not None: - self.altitude_mode = altitude_mode.text - else: - altitude_mode = element.find(f"{gx.NS}altitudeMode") - self.altitude_mode = altitude_mode.text - - def etree_element(self): - element = super().etree_element() - if self.longitude: - longitude = config.etree.SubElement(element, f"{self.ns}longitude") - longitude.text = self._longitude - if self.latitude: - latitude = config.etree.SubElement(element, f"{self.ns}latitude") - latitude.text = self.latitude - if self.altitude: - altitude = config.etree.SubElement(element, f"{self.ns}altitude") - altitude.text = self._altitude - if self.heading: - heading = config.etree.SubElement(element, f"{self.ns}heading") - heading.text = self._heading - if self.tilt: - tilt = config.etree.SubElement(element, f"{self.ns}tilt") - tilt.text = self._tilt - if self.range: - range_var = config.etree.SubElement(element, f"{self.ns}range") - range_var.text = self._range - if self.altitude_mode in ("clampedToGround", "relativeToGround", "absolute"): - altitude_mode = config.etree.SubElement(element, f"{self.ns}altitudeMode") - elif self.altitude_mode in ("clampedToSeaFloor", "relativeToSeaFloor"): - altitude_mode = config.etree.SubElement(element, f"{gx.NS}altitudeMode") - altitude_mode.text = self.altitude_mode - return element - - class KML: """represents a KML File""" diff --git a/fastkml/overlays.py b/fastkml/overlays.py deleted file mode 100644 index e69de29b..00000000 diff --git a/fastkml/times.py b/fastkml/times.py index e69de29b..ac367ce3 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -0,0 +1,183 @@ +"""Date and time handling in KML.""" +from datetime import date +from datetime import datetime +from typing import List +from typing import Optional +from typing import Union + +# note that there are some ISO 8601 timeparsers at pypi +# but in my tests all of them had some errors so we rely on the +# tried and tested dateutil here which is more stable. As a side effect +# we can also parse non ISO compliant dateTimes +import dateutil.parser + +import fastkml.config as config +from fastkml.base import _BaseObject +from fastkml.types import Element + + +class _TimePrimitive(_BaseObject): + """The dateTime is defined according to XML Schema time. + The value can be expressed as yyyy-mm-ddThh:mm:sszzzzzz, where T is + the separator between the date and the time, and the time zone is + either Z (for UTC) or zzzzzz, which represents ±hh:mm in relation to + UTC. Additionally, the value can be expressed as a date only. + + The precision of the dateTime is dictated by the dateTime value + which can be one of the following: + + - dateTime gives second resolution + - date gives day resolution + - gYearMonth gives month resolution + - gYear gives year resolution + """ + + RESOLUTIONS = ["gYear", "gYearMonth", "date", "dateTime"] + + def get_resolution( + self, + dt: Optional[Union[date, datetime]], + resolution: Optional[str] = None, + ) -> Optional[str]: + if resolution: + if resolution not in self.RESOLUTIONS: + raise ValueError + else: + return resolution + elif isinstance(dt, datetime): + resolution = "dateTime" + elif isinstance(dt, date): + resolution = "date" + else: + resolution = None + return resolution + + def parse_str(self, datestr: str) -> List[Union[datetime, str]]: + resolution = "dateTime" + year = 0 + month = 1 + day = 1 + if len(datestr) == 4: + resolution = "gYear" + year = int(datestr) + dt = datetime(year, month, day) + elif len(datestr) == 6: + resolution = "gYearMonth" + year = int(datestr[:4]) + month = int(datestr[-2:]) + dt = datetime(year, month, day) + elif len(datestr) == 7: + resolution = "gYearMonth" + year = int(datestr.split("-")[0]) + month = int(datestr.split("-")[1]) + dt = datetime(year, month, day) + elif len(datestr) in [8, 10]: + resolution = "date" + dt = dateutil.parser.parse(datestr) + elif len(datestr) > 10: + resolution = "dateTime" + dt = dateutil.parser.parse(datestr) + else: + raise ValueError + return [dt, resolution] + + def date_to_string( + self, + dt: Optional[Union[date, datetime]], + resolution: Optional[str] = None, + ) -> Optional[str]: + if isinstance(dt, (date, datetime)): + resolution = self.get_resolution(dt, resolution) + if resolution == "gYear": + return dt.strftime("%Y") + elif resolution == "gYearMonth": + return dt.strftime("%Y-%m") + elif resolution == "date": + return ( + dt.date().isoformat() + if isinstance(dt, datetime) + else dt.isoformat() + ) + elif resolution == "dateTime": + return dt.isoformat() + + +class TimeStamp(_TimePrimitive): + """Represents a single moment in time.""" + + __name__ = "TimeStamp" + timestamp = None + + def __init__( + self, + ns: Optional[str] = None, + id: None = None, + timestamp: Optional[Union[date, datetime]] = None, + resolution: None = None, + ) -> None: + super().__init__(ns, id) + resolution = self.get_resolution(timestamp, resolution) + self.timestamp = [timestamp, resolution] + + def etree_element(self) -> Element: + element = super().etree_element() + when = config.etree.SubElement(element, f"{self.ns}when") + when.text = self.date_to_string(*self.timestamp) + return element + + def from_element(self, element: Element) -> None: + super().from_element(element) + when = element.find(f"{self.ns}when") + if when is not None: + self.timestamp = self.parse_str(when.text) + + +class TimeSpan(_TimePrimitive): + """Represents an extent in time bounded by begin and end dateTimes.""" + + __name__ = "TimeSpan" + begin = None + end = None + + def __init__( + self, + ns: Optional[str] = None, + id: None = None, + begin: Optional[Union[date, datetime]] = None, + begin_res: None = None, + end: Optional[Union[date, datetime]] = None, + end_res: None = None, + ) -> None: + super().__init__(ns, id) + if begin: + resolution = self.get_resolution(begin, begin_res) + self.begin = [begin, resolution] + if end: + resolution = self.get_resolution(end, end_res) + self.end = [end, resolution] + + def from_element(self, element: Element) -> None: + super().from_element(element) + begin = element.find(f"{self.ns}begin") + if begin is not None: + self.begin = self.parse_str(begin.text) + end = element.find(f"{self.ns}end") + if end is not None: + self.end = self.parse_str(end.text) + + def etree_element(self) -> Element: + element = super().etree_element() + if self.begin is not None: + text = self.date_to_string(*self.begin) + if text: + begin = config.etree.SubElement(element, f"{self.ns}begin") + begin.text = text + if self.end is not None: + text = self.date_to_string(*self.end) + if text: + end = config.etree.SubElement(element, f"{self.ns}end") + end.text = text + if self.begin == self.end is None: + raise ValueError("Either begin, end or both must be set") + # TODO test if end > begin + return element diff --git a/fastkml/views.py b/fastkml/views.py index e69de29b..2bcc5db5 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -0,0 +1,520 @@ +import logging +from typing import Optional + +import fastkml.config as config +import fastkml.gx as gx +from fastkml.base import _BaseObject +from fastkml.times import TimeSpan +from fastkml.times import TimeStamp +from fastkml.types import Element + +logger = logging.getLogger(__name__) + + +class _AbstractView(_BaseObject): + """ + This is an abstract element and cannot be used directly in a KML file. + This element is extended by the and elements. + """ + + _gx_timespan = None + _gx_timestamp = None + + def etree_element(self): + element = super().etree_element() + if (self._timespan is not None) and (self._timestamp is not None): + raise ValueError("Either Timestamp or Timespan can be defined, not both") + if self._timespan is not None: + element.append(self._gx_timespan.etree_element()) + elif self._timestamp is not None: + element.append(self._gx_timestamp.etree_element()) + return element + + @property + def gx_timestamp(self): + return self._gx_timestamp + + @gx_timestamp.setter + def gx_timestamp(self, dt): + self._gx_timestamp = None if dt is None else TimeStamp(timestamp=dt) + if self._gx_timestamp is not None: + logger.warning("Setting a TimeStamp, TimeSpan deleted") + self._gx_timespan = None + + @property + def begin(self): + return self._gx_timespan.begin[0] + + @begin.setter + def begin(self, dt) -> None: + if self._gx_timespan is None: + self._gx_timespan = TimeSpan(begin=dt) + elif self._gx_timespan.begin is None: + self._gx_timespan.begin = [dt, None] + else: + self._gx_timespan.begin[0] = dt + if self._gx_timestamp is not None: + logger.warning("Setting a TimeSpan, TimeStamp deleted") + self._gx_timestamp = None + + @property + def end(self): + return self._gx_timespan.end[0] + + @end.setter + def end(self, dt): + if self._gx_timespan is None: + self._gx_timespan = TimeSpan(end=dt) + elif self._gx_timespan.end is None: + self._gx_timespan.end = [dt, None] + else: + self._gx_timespan.end[0] = dt + if self._gx_timestamp is not None: + logger.warning("Setting a TimeSpan, TimeStamp deleted") + self._gx_timestamp = None + + def from_element(self, element): + super().from_element(element) + gx_timespan = element.find(f"{gx.NS}TimeSpan") + if gx_timespan is not None: + self._gx_timespan = gx_timespan.text + gx_timestamp = element.find(f"{gx.NS}TimeStamp") + if gx_timestamp is not None: + self._gx_timestamp = gx_timestamp.text + + # TODO: + # TODO: + + +class Camera(_AbstractView): + """ + Defines the virtual camera that views the scene. This element defines + the position of the camera relative to the Earth's surface as well + as the viewing direction of the camera. The camera position is defined + by , , , and either or + . The viewing direction of the camera is defined by + , , and . can be a child element of any + Feature or of . A parent element cannot contain both a + and a at the same time. + + provides full six-degrees-of-freedom control over the view, + so you can position the Camera in space and then rotate it around the + X, Y, and Z axes. Most importantly, you can tilt the camera view so that + you're looking above the horizon into the sky. + + can also contain a TimePrimitive ( or ). + Time values in Camera affect historical imagery, sunlight, and the display of + time-stamped features. For more information, read Time with AbstractViews in + the Time and Animation chapter of the Developer's Guide. + """ + + __name__ = "Camera" + + _longitude = None + # Longitude of the virtual camera (eye point). Angular distance in degrees, + # relative to the Prime Meridian. Values west of the Meridian range from + # −180 to 0 degrees. Values east of the Meridian range from 0 to 180 degrees. + + _latitude = None + # Latitude of the virtual camera. Degrees north or south of the Equator + # (0 degrees). Values range from −90 degrees to 90 degrees. + + _altitude = None + # Distance of the camera from the earth's surface, in meters. Interpreted + # according to the Camera's or . + + _heading = None + # Direction (azimuth) of the camera, in degrees. Default=0 (true North). + # (See diagram.) Values range from 0 to 360 degrees. + + _tilt = None + # Rotation, in degrees, of the camera around the X axis. A value of 0 + # indicates that the view is aimed straight down toward the earth (the + # most common case). A value for 90 for indicates that the view + # is aimed toward the horizon. Values greater than 90 indicate that the + # view is pointed up into the sky. Values for are clamped at +180 + # degrees. + + _roll = None + # Rotation, in degrees, of the camera around the Z axis. Values range from + # −180 to +180 degrees. + + _altitude_mode = "relativeToGround" + # Specifies how the specified for the Camera is interpreted. + # Possible values are as follows: + # relativeToGround - + # (default) Interprets the as a value in meters above the + # ground. If the point is over water, the will be + # interpreted as a value in meters above sea level. See + # below to specify points relative to the sea floor. + # clampToGround - + # For a camera, this setting also places the camera relativeToGround, + # since putting the camera exactly at terrain height would mean that + # the eye would intersect the terrain (and the view would be blocked). + # absolute - + # Interprets the as a value in meters above sea level. + + def __init__( + self, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + longitude: Optional[float] = None, + latitude: Optional[float] = None, + altitude: Optional[float] = None, + heading: Optional[float] = None, + tilt: Optional[float] = None, + roll: Optional[float] = None, + altitude_mode: str = "relativeToGround", + ) -> None: + super().__init__(ns=ns, id=id, target_id=target_id) + self._longitude = longitude + self._latitude = latitude + self._altitude = altitude + self._heading = heading + self._tilt = tilt + self._roll = roll + self._altitude_mode = altitude_mode + + @property + def longitude(self) -> Optional[float]: + return self._longitude + + @longitude.setter + def longitude(self, value) -> None: + if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): + self._longitude = str(value) + elif value is None: + self._longitude = None + else: + raise ValueError + + @property + def latitude(self) -> Optional[float]: + return self._latitude + + @latitude.setter + def latitude(self, value) -> None: + if isinstance(value, (str, int, float)) and (-90 <= float(value) <= 90): + self._latitude = str(value) + elif value is None: + self._latitude = None + else: + raise ValueError + + @property + def altitude(self) -> Optional[float]: + return self._altitude + + @altitude.setter + def altitude(self, value) -> None: + if isinstance(value, (str, int, float)): + self._altitude = str(value) + elif value is None: + self._altitude = None + else: + raise ValueError + + @property + def heading(self) -> Optional[float]: + return self._heading + + @heading.setter + def heading(self, value) -> None: + if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): + self._heading = str(value) + elif value is None: + self._heading = None + else: + raise ValueError + + @property + def tilt(self) -> Optional[float]: + return self._tilt + + @tilt.setter + def tilt(self, value) -> None: + if isinstance(value, (str, int, float)) and (0 <= float(value) <= 180): + self._tilt = str(value) + elif value is None: + self._tilt = None + else: + raise ValueError + + @property + def roll(self) -> Optional[float]: + return self._roll + + @roll.setter + def roll(self, value) -> None: + if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): + self._roll = str(value) + elif value is None: + self._roll = None + else: + raise ValueError + + @property + def altitude_mode(self) -> str: + return self._altitude_mode + + @altitude_mode.setter + def altitude_mode(self, mode) -> None: + if mode in ("relativeToGround", "clampToGround", "absolute"): + self._altitude_mode = str(mode) + else: + self._altitude_mode = "relativeToGround" + # raise ValueError( + # "altitude_mode must be one of " "relativeToGround, + # clampToGround, absolute") + + def from_element(self, element) -> None: + super().from_element(element) + longitude = element.find(f"{self.ns}longitude") + if longitude is not None: + self.longitude = longitude.text + latitude = element.find(f"{self.ns}latitude") + if latitude is not None: + self.latitude = latitude.text + altitude = element.find(f"{self.ns}altitude") + if altitude is not None: + self.altitude = altitude.text + heading = element.find(f"{self.ns}heading") + if heading is not None: + self.heading = heading.text + tilt = element.find(f"{self.ns}tilt") + if tilt is not None: + self.tilt = tilt.text + roll = element.find(f"{self.ns}roll") + if roll is not None: + self.roll = roll.text + altitude_mode = element.find(f"{self.ns}altitudeMode") + if altitude_mode is not None: + self.altitude_mode = altitude_mode.text + else: + altitude_mode = element.find(f"{gx.NS}altitudeMode") + self.altitude_mode = altitude_mode.text + + def etree_element(self) -> Element: + element = super().etree_element() + if self.longitude: + longitude = config.etree.SubElement(element, f"{self.ns}longitude") + longitude.text = self.longitude + if self.latitude: + latitude = config.etree.SubElement(element, f"{self.ns}latitude") + latitude.text = self.latitude + if self.altitude: + altitude = config.etree.SubElement(element, f"{self.ns}altitude") + altitude.text = self.altitude + if self.heading: + heading = config.etree.SubElement(element, f"{self.ns}heading") + heading.text = self.heading + if self.tilt: + tilt = config.etree.SubElement(element, f"{self.ns}tilt") + tilt.text = self.tilt + if self.roll: + roll = config.etree.SubElement(element, f"{self.ns}roll") + roll.text = self.roll + if self.altitude_mode in ("clampedToGround", "relativeToGround", "absolute"): + altitude_mode = config.etree.SubElement(element, f"{self.ns}altitudeMode") + elif self.altitude_mode in ("clampedToSeaFloor", "relativeToSeaFloor"): + altitude_mode = config.etree.SubElement(element, f"{gx.NS}altitudeMode") + altitude_mode.text = self.altitude_mode + return element + + +class LookAt(_AbstractView): + + _longitude = None + # Longitude of the point the camera is looking at. Angular distance in + # degrees, relative to the Prime Meridian. Values west of the Meridian + # range from −180 to 0 degrees. Values east of the Meridian range from + # 0 to 180 degrees. + + _latitude = None + # Latitude of the point the camera is looking at. Degrees north or south + # of the Equator (0 degrees). Values range from −90 degrees to 90 degrees. + + _altitude = None + # Distance from the earth's surface, in meters. Interpreted according to + # the LookAt's altitude mode. + + _heading = None + # Direction (that is, North, South, East, West), in degrees. Default=0 + # (North). (See diagram below.) Values range from 0 to 360 degrees. + + _tilt = None + # Angle between the direction of the LookAt position and the normal to the + # surface of the earth. (See diagram below.) Values range from 0 to 90 + # degrees. Values for cannot be negative. A value of 0 + # degrees indicates viewing from directly above. A value of 90 + # degrees indicates viewing along the horizon. + + _range = None + # Distance in meters from the point specified by , , + # and to the LookAt position. (See diagram below.) + + _altitude_mode = None + # Specifies how the specified for the LookAt point is + # interpreted. Possible values are as follows: + # clampToGround - + # (default) Indicates to ignore the specification and + # place the LookAt position on the ground. + # relativeToGround - + # Interprets the as a value in meters above the ground. + # absolute - + # Interprets the as a value in meters above sea level. + + @property + def longitude(self): + return self._longitude + + @longitude.setter + def longitude(self, value): + if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): + self._longitude = str(value) + elif value is None: + self._longitude = None + else: + raise ValueError + + @property + def latitude(self): + return self._latitude + + @latitude.setter + def latitude(self, value): + if isinstance(value, (str, int, float)) and (-90 <= float(value) <= 90): + self._latitude = str(value) + elif value is None: + self._latitude = None + else: + raise ValueError + + @property + def altitude(self): + return self._altitude + + @altitude.setter + def altitude(self, value): + if isinstance(value, (str, int, float)): + self._altitude = str(value) + elif value is None: + self._altitude = None + else: + raise ValueError + + @property + def heading(self): + return self._heading + + @heading.setter + def heading(self, value): + if isinstance(value, (str, int, float)): + self._heading = str(value) + elif value is None: + self._heading = None + else: + raise ValueError + + @property + def tilt(self): + return self._tilt + + @tilt.setter + def tilt(self, value): + if isinstance(value, (str, int, float)) and (0 <= float(value) <= 90): + self._tilt = str(value) + elif value is None: + self._tilt = None + else: + raise ValueError + + @property + def range(self): + return self._range + + @range.setter + def range(self, value): + if isinstance(value, (str, int, float)): + self._range = str(value) + elif value is None: + self._range = None + else: + raise ValueError + + @property + def altitude_mode(self): + return self._altitude_mode + + @altitude_mode.setter + def altitude_mode(self, mode): + if mode in ( + "relativeToGround", + "clampToGround", + "absolute", + "relativeToSeaFloor", + "clampToSeaFloor", + ): + self._altitude_mode = str(mode) + else: + self._altitude_mode = "relativeToGround" + # raise ValueError( + # "altitude_mode must be one of " + # + "relativeToGround, clampToGround, absolute, + # + relativeToSeaFloor, clampToSeaFloor" + # ) + + def from_element(self, element): + super().from_element(element) + longitude = element.find(f"{self.ns}longitude") + if longitude is not None: + self.longitude = longitude.text + latitude = element.find(f"{self.ns}latitude") + if latitude is not None: + self.latitude = latitude.text + altitude = element.find(f"{self.ns}altitude") + if altitude is not None: + self.altitude = altitude.text + heading = element.find(f"{self.ns}heading") + if heading is not None: + self.heading = heading.text + tilt = element.find(f"{self.ns}tilt") + if tilt is not None: + self.tilt = tilt.text + range_var = element.find(f"{self.ns}range") + if range_var is not None: + self.range = range_var.text + altitude_mode = element.find(f"{self.ns}altitudeMode") + if altitude_mode is not None: + self.altitude_mode = altitude_mode.text + else: + altitude_mode = element.find(f"{gx.NS}altitudeMode") + self.altitude_mode = altitude_mode.text + + def etree_element(self): + element = super().etree_element() + if self.longitude: + longitude = config.etree.SubElement(element, f"{self.ns}longitude") + longitude.text = self._longitude + if self.latitude: + latitude = config.etree.SubElement(element, f"{self.ns}latitude") + latitude.text = self.latitude + if self.altitude: + altitude = config.etree.SubElement(element, f"{self.ns}altitude") + altitude.text = self._altitude + if self.heading: + heading = config.etree.SubElement(element, f"{self.ns}heading") + heading.text = self._heading + if self.tilt: + tilt = config.etree.SubElement(element, f"{self.ns}tilt") + tilt.text = self._tilt + if self.range: + range_var = config.etree.SubElement(element, f"{self.ns}range") + range_var.text = self._range + if self.altitude_mode in ("clampedToGround", "relativeToGround", "absolute"): + altitude_mode = config.etree.SubElement(element, f"{self.ns}altitudeMode") + elif self.altitude_mode in ("clampedToSeaFloor", "relativeToSeaFloor"): + altitude_mode = config.etree.SubElement(element, f"{gx.NS}altitudeMode") + altitude_mode.text = self.altitude_mode + return element From c71b80a0b618d49022a2940c3cc7ff5ec95e3e17 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 9 Oct 2022 12:14:38 +0100 Subject: [PATCH 137/144] relax mypy for new files --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9f6fd0f4..b8460005 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,5 +39,8 @@ show_error_codes = true # mypy per-module options: [[tool.mypy.overrides]] -module = ["fastkml.tests.oldunit_test", "fastkml.tests.config_test"] +module = [ + "fastkml.kml", "fastkml.data", "fastkml.views", "fastkml.times", + "fastkml.tests.oldunit_test", "fastkml.tests.config_test" +] ignore_errors = true From 11dd6b18a13c4f17d49b8b5e0f5f67f5b8843b6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Oct 2022 15:50:15 +0000 Subject: [PATCH 138/144] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- fastkml/atom.py | 3 +-- fastkml/base.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/fastkml/atom.py b/fastkml/atom.py index dc6992a2..9ae52ce3 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -37,6 +37,7 @@ from typing import Tuple from fastkml.base import _XMLObject +from fastkml.config import ATOMNS as NS from fastkml.helpers import o_from_attr from fastkml.helpers import o_from_subelement_text from fastkml.helpers import o_int_from_attr @@ -45,8 +46,6 @@ from fastkml.types import Element from fastkml.types import KmlObjectMap -from .config import ATOMNS as NS - logger = logging.getLogger(__name__) regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" email_match = re.compile(regex).match diff --git a/fastkml/base.py b/fastkml/base.py index c8ea3003..7b2defba 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -21,12 +21,11 @@ from typing import cast from fastkml import config +from fastkml.helpers import o_from_attr +from fastkml.helpers import o_to_attr from fastkml.types import Element from fastkml.types import KmlObjectMap -from .helpers import o_from_attr -from .helpers import o_to_attr - logger = logging.getLogger(__name__) From 564930f2d09b3f2d0772f7a01de6fa6bd2f04fbe Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 9 Oct 2022 17:00:20 +0100 Subject: [PATCH 139/144] split into smaller modules --- fastkml/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastkml/data.py b/fastkml/data.py index 043dea59..55eafc69 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -14,7 +14,7 @@ class SimpleField(TypedDict): name: str type: str - displayName: str + displayName: str # nqa: N815 class Schema(_BaseObject): From f4fda7943602c3328866b1485c00115422a0bd54 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 9 Oct 2022 17:06:51 +0100 Subject: [PATCH 140/144] fix mispelled noqa --- fastkml/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastkml/data.py b/fastkml/data.py index 55eafc69..4e29c3ef 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -14,7 +14,7 @@ class SimpleField(TypedDict): name: str type: str - displayName: str # nqa: N815 + displayName: str # noqa: N815 class Schema(_BaseObject): From c095cef55c9cc411be0583f960becd6a0d90151a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 9 Oct 2022 17:15:58 +0100 Subject: [PATCH 141/144] fix imports in __init__ --- fastkml/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fastkml/__init__.py b/fastkml/__init__.py index d78d8ea1..ca997c73 100644 --- a/fastkml/__init__.py +++ b/fastkml/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 Christian Ledermann +# Copyright (C) 2012 -2022 Christian Ledermann # # This library is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free @@ -34,12 +34,11 @@ from fastkml.data import ExtendedData from fastkml.data import Schema from fastkml.data import SchemaData +from fastkml.gx import GxGeometry from fastkml.kml import KML from fastkml.kml import Document from fastkml.kml import Folder from fastkml.kml import Placemark -from fastkml.kml import TimeSpan -from fastkml.kml import TimeStamp from fastkml.styles import BalloonStyle from fastkml.styles import IconStyle from fastkml.styles import LabelStyle @@ -48,6 +47,8 @@ from fastkml.styles import Style from fastkml.styles import StyleMap from fastkml.styles import StyleUrl +from fastkml.times import TimeSpan +from fastkml.times import TimeStamp try: __version__ = get_distribution("fastkml").version @@ -63,6 +64,7 @@ "TimeStamp", "ExtendedData", "Data", + "GxGeometry", "Schema", "SchemaData", "StyleUrl", From a9d877bec0bfba23086324aea127fb5498ebded4 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 9 Oct 2022 17:36:29 +0100 Subject: [PATCH 142/144] start fixing types in times --- fastkml/times.py | 7 +++++-- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/fastkml/times.py b/fastkml/times.py index ac367ce3..f076aaa6 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -100,6 +100,7 @@ def date_to_string( ) elif resolution == "dateTime": return dt.isoformat() + return None class TimeStamp(_TimePrimitive): @@ -113,7 +114,7 @@ def __init__( ns: Optional[str] = None, id: None = None, timestamp: Optional[Union[date, datetime]] = None, - resolution: None = None, + resolution: Optional[str] = None, ) -> None: super().__init__(ns, id) resolution = self.get_resolution(timestamp, resolution) @@ -121,7 +122,9 @@ def __init__( def etree_element(self) -> Element: element = super().etree_element() - when = config.etree.SubElement(element, f"{self.ns}when") + when = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}when" + ) when.text = self.date_to_string(*self.timestamp) return element diff --git a/pyproject.toml b/pyproject.toml index b8460005..8e25a235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ show_error_codes = true [[tool.mypy.overrides]] module = [ - "fastkml.kml", "fastkml.data", "fastkml.views", "fastkml.times", + "fastkml.kml", "fastkml.data", "fastkml.views", "fastkml.tests.oldunit_test", "fastkml.tests.config_test" ] ignore_errors = true From 406758069e92405382f6fc4f829ca57de77c93ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:47:35 +0000 Subject: [PATCH 143/144] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) - https://gitlab.com/pycqa/flake8 → https://github.com/PyCQA/flake8 - [github.com/PyCQA/flake8: 3.9.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/3.9.2...5.0.4) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 729e8742..07772f57 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,11 +21,11 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pycqa/isort From cfd9935943fc3474fdc91e94aa53f8f3afbbcff7 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 12 Oct 2022 13:33:18 +0100 Subject: [PATCH 144/144] fastkml.times needs to be correctly annotated --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8e25a235..b8460005 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ show_error_codes = true [[tool.mypy.overrides]] module = [ - "fastkml.kml", "fastkml.data", "fastkml.views", + "fastkml.kml", "fastkml.data", "fastkml.views", "fastkml.times", "fastkml.tests.oldunit_test", "fastkml.tests.config_test" ] ignore_errors = true