././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1711389497.435458 pygeoif-1.4.0/0000755000175100001770000000000014600335471012566 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1711389497.435458 pygeoif-1.4.0/PKG-INFO0000644000175100001770000003430014600335471013663 0ustar00runnerdockerMetadata-Version: 2.1 Name: pygeoif Version: 1.4.0 Summary: A basic implementation of the __geo_interface__ Author-email: Christian Ledermann License: LGPL Project-URL: Changelog, https://github.com/cleder/pygeoif/blob/develop/docs/HISTORY.rst Project-URL: Documentation, https://pygeoif.readthedocs.io/ Project-URL: Homepage, https://github.com/cleder/pygeoif/ Keywords: GIS,Spatial,WKT Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Scientific/Engineering :: GIS Classifier: Typing :: Typed Requires-Python: >=3.8 Description-Content-Type: text/x-rst Requires-Dist: typing_extensions Provides-Extra: complexity Requires-Dist: lizard; extra == "complexity" Requires-Dist: radon; extra == "complexity" Provides-Extra: dev Requires-Dist: pre-commit; extra == "dev" Requires-Dist: pygeoif[complexity]; extra == "dev" Requires-Dist: pygeoif[linting]; extra == "dev" Requires-Dist: pygeoif[tests]; extra == "dev" Requires-Dist: pygeoif[typing]; extra == "dev" Provides-Extra: hypothesis Requires-Dist: hypothesis; extra == "hypothesis" Provides-Extra: linting Requires-Dist: black; extra == "linting" Requires-Dist: flake8; extra == "linting" Requires-Dist: flake8-cognitive-complexity; extra == "linting" Requires-Dist: flake8-comments; extra == "linting" Requires-Dist: flake8-complex-f-strings; extra == "linting" Requires-Dist: flake8-continuation; extra == "linting" Requires-Dist: flake8-docstrings; extra == "linting" Requires-Dist: flake8-dunder-all; extra == "linting" Requires-Dist: flake8-encodings; extra == "linting" Requires-Dist: flake8-expression-complexity; extra == "linting" Requires-Dist: flake8-function-order; extra == "linting" Requires-Dist: flake8-length; extra == "linting" Requires-Dist: flake8-pep3101; extra == "linting" Requires-Dist: flake8-rst-docstrings; extra == "linting" Requires-Dist: flake8-string-format; extra == "linting" Requires-Dist: flake8-super; extra == "linting" Requires-Dist: flake8-typing-imports; extra == "linting" Requires-Dist: flake8-use-fstring; extra == "linting" Requires-Dist: ruff; extra == "linting" Requires-Dist: yamllint; extra == "linting" Provides-Extra: tests Requires-Dist: hypothesis; extra == "tests" Requires-Dist: pytest; extra == "tests" Requires-Dist: pytest-cov; extra == "tests" Provides-Extra: typing Requires-Dist: hypothesis; extra == "typing" Requires-Dist: mypy; extra == "typing" Introduction ============ .. inclusion-marker-do-not-remove PyGeoIf provides a `GeoJSON-like protocol `_ for geo-spatial (GIS) vector data. Other Python programs and packages that you may have heard of that implement this protocol: * `ArcPy `_ * `descartes `_ * `PySAL `_ * `Shapely `_ * `pyshp `_ * `GeoPandas `_ * `Karta `_ * `mapnik `_ When you want to write your own geospatial library with support for this protocol you may use pygeoif as a starting point and build your functionality on top of it. It has no requirements outside the Python standard library and is therefore easy to integrate into your project. It is tested on `CPython `_ and `PyPy `_, but it should work on alternative Python implementations (that implement the language specification *>=3.8*) as well. You may think of pygeoif as a 'shapely ultralight' which lets you construct geometries and perform **very** basic operations like reading and writing geometries from/to WKT, constructing line strings out of points, polygons from linear rings, multi polygons from polygons, etc. It was inspired by shapely and implements the geometries in a way that when you are familiar with pygeoif, you will feel right at home with shapely or the other way round. It provides Hypothesis strategies for all geometries for property based testing with Hypothesis_. It was written to provide clean and python only geometries for fastkml_ .. image:: https://readthedocs.org/projects/pygeoif/badge/?version=latest :target: https://pygeoif.readthedocs.io/en/latest/?badge=latest :alt: Documentation .. image:: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml/badge.svg?branch=main :target: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml :alt: GitHub Actions .. image:: https://codecov.io/gh/cleder/pygeoif/branch/main/graph/badge.svg?token=2EfiwBXs9X :target: https://codecov.io/gh/cleder/pygeoif :alt: Codecov .. image:: https://img.shields.io/badge/property_based_tests-hypothesis-green :target: https://hypothesis.works :alt: Hypothesis .. image:: https://img.shields.io/badge/code_style-black-000000.svg :target: https://github.com/psf/ :alt: Black .. image:: https://img.shields.io/badge/type_checker-mypy-blue :target: http://mypy-lang.org/ :alt: Mypy .. image:: https://www.openhub.net/p/pygeoif/widgets/project_thin_badge.gif :target: https://www.openhub.net/p/pygeoif/ :alt: Openhub .. image:: https://www.codefactor.io/repository/github/cleder/pygeoif/badge/main :target: https://www.codefactor.io/repository/github/cleder/pygeoif/overview/main :alt: CodeFactor .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit :target: https://github.com/pre-commit/pre-commit :alt: pre-commit .. image:: https://img.shields.io/pypi/pyversions/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/implementation/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python implementations .. image:: https://img.shields.io/pypi/v/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: License .. image:: https://img.shields.io/pypi/dm/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Downloads Installation ------------ You can install PyGeoIf from pypi using pip:: pip install pygeoif Example ======== >>> from pygeoif import geometry >>> p = geometry.Point(1,1) >>> p.__geo_interface__ {'type': 'Point', 'bbox': (1, 1, 1, 1), 'coordinates': (1, 1)} >>> print(p) POINT (1 1) >>> p Point(1, 1) >>> l = geometry.LineString([(0.0, 0.0), (1.0, 1.0)]) >>> l.bounds (0.0, 0.0, 1.0, 1.0) >>> print(l) LINESTRING (0.0 0.0, 1.0 1.0) You find more examples in the `tests `_ directory which cover every aspect of pygeoif or in fastkml_. Classes ======== All classes implement the attribute: * ``__geo_interface__``: as discussed above, an interface to GeoJSON_. All geometry classes implement the attributes: * ``geom_type``: Returns a string specifying the Geometry Type of the object * ``bounds``: Returns a (minx, miny, maxx, maxy) tuple that bounds the object. * ``wkt``: Returns the 'Well Known Text' representation of the object For two-dimensional geometries the following methods are implemented: * ``convex_hull``: Returns a representation of the smallest convex Polygon containing all the points in the object unless the number of points in the object is less than three. For two points, the convex hull collapses to a LineString; for 1, a Point. For three dimensional objects only their projection in the xy plane is taken into consideration. Empty objects without coordinates return ``None`` for the convex_hull. Point ----- A zero dimensional geometry A point has zero length and zero area. A point cannot be empty. Attributes ~~~~~~~~~~~ x, y, z : float Coordinate values Example ~~~~~~~~ >>> from pygeoif import Point >>> p = Point(1.0, -1.0) >>> print(p) POINT (1.0 -1.0) >>> p.y -1.0 >>> p.x 1.0 LineString ----------- A one-dimensional figure comprising one or more line segments A LineString has non-zero length and zero area. It may approximate a curve and need not be straight. Unlike a LinearRing, a LineString is not closed. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of Points LinearRing ----------- A closed one-dimensional geometry comprising one or more line segments A LinearRing that crosses itself or touches itself at a single point is invalid and operations on it may fail. A LinearRing is self closing. Polygon -------- A two-dimensional figure bounded by a linear ring A polygon has a non-zero area. It may have one or more negative-space "holes" which are also bounded by linear rings. If any rings cross each other, the geometry is invalid and operations on it may fail. Attributes ~~~~~~~~~~~ exterior : LinearRing The ring which bounds the positive space of the polygon. interiors : sequence A sequence of rings which bound all existing holes. maybe_valid: boolean When a polygon has obvious problems such as self crossing lines or holes that are outside the exterior bounds this will return False. Even if this returns True the geometry may still be invalid, but if this returns False you do have a problem. MultiPoint ---------- A collection of one or more points. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of Points. MultiLineString ---------------- A collection of one or more line strings. A MultiLineString has non-zero length and zero area. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of LineStrings MultiPolygon ------------- A collection of one or more polygons. Attributes ~~~~~~~~~~~~~ geoms : sequence A sequence of `Polygon` instances GeometryCollection ------------------- A heterogenous collection of geometries (Points, LineStrings, LinearRings and Polygons). Attributes ~~~~~~~~~~~ geoms : sequence A sequence of geometry instances Please note: ``GEOMETRYCOLLECTION`` isn't supported by the Shapefile or GeoJSON_ format. And this sub-class isn't generally supported by ordinary GIS sw (viewers and so on). So it's very rarely used in the real GIS professional world. Example ~~~~~~~~ >>> from pygeoif import geometry >>> p = geometry.Point(1.0, -1.0) >>> p2 = geometry.Point(1.0, -1.0) >>> geoms = [p, p2] >>> c = geometry.GeometryCollection(geoms) >>> [geom for geom in geoms] [Point(1.0, -1.0), Point(1.0, -1.0)] Feature ------- Aggregates a geometry instance with associated user-defined properties. Attributes ~~~~~~~~~~~ geometry : object A geometry instance properties : dict A dictionary linking field keys with values associated with with geometry instance Example ~~~~~~~~ >>> from pygeoif import Point, Feature >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = Feature(p, props) >>> a.properties {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a.properties['Name'] 'Sample Point' FeatureCollection ----------------- A heterogenous collection of Features Attributes ~~~~~~~~~~~ features: sequence A sequence of feature instances Example ~~~~~~~~ >>> from pygeoif import Point, Feature, FeatureCollection >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = Feature(p, props) >>> p2 = Point(1.0, -1.0) >>> props2 = {'Name': 'Sample Point2', 'Other': 'Other Data2'} >>> b = Feature(p2, props2) >>> features = [a, b] >>> c = FeatureCollection(features) >>> [feature for feature in c] [Feature(Point(1.0, -1.0), {'Name': 'Sample Point', 'Other': 'Other Data'},...] Functions ========= shape -------- Create a pygeoif feature from an object that provides the ``__geo_interface__`` or any GeoJSON_ compatible dictionary. >>> from shapely.geometry import Point >>> from pygeoif import geometry, shape >>> shape(Point(0,0)) Point(0.0, 0.0) from_wkt --------- Create a geometry from its WKT representation >>> from pygeoif import from_wkt >>> p = from_wkt('POINT (0 1)') >>> print(p) POINT (0.0 1.0) signed_area ------------ Return the signed area enclosed by a ring. A value >= 0 indicates a counter-clockwise oriented ring. orient ------- Returns a copy of a polygon with exteriors and interiors in the right orientation. if ccw is True than the exterior will be in counterclockwise orientation and the interiors will be in clockwise orientation, or the other way round when ccw is False. box --- Return a rectangular polygon with configurable normal vector. mapping ------- Return the ``__geo_interface__`` dictionary. Development =========== Clone this repository, create a virtualenv with Python 3.8 or later with ``python3 -m venv .venv`` and activate it with ``source .venv/bin/activate``. Then install the requirements with ``pip install -e ".[dev]"``. 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 Testing ------- Run the unit and static tests with:: pytest tests pytest --doctest-glob="README.rst" black pygeoif ruff pygeoif flake8 pygeoif mypy pygeoif Acknowledgments ================ The tests were improved with mutmut_ which discovered some nasty edge cases. .. _mutmut: https://github.com/boxed/mutmut .. _GeoJSON: https://geojson.org/ .. _fastkml: http://pypi.python.org/pypi/fastkml/ .. _Hypothesis: https://hypothesis.works ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/README.rst0000644000175100001770000002617414600335465014272 0ustar00runnerdockerIntroduction ============ .. inclusion-marker-do-not-remove PyGeoIf provides a `GeoJSON-like protocol `_ for geo-spatial (GIS) vector data. Other Python programs and packages that you may have heard of that implement this protocol: * `ArcPy `_ * `descartes `_ * `PySAL `_ * `Shapely `_ * `pyshp `_ * `GeoPandas `_ * `Karta `_ * `mapnik `_ When you want to write your own geospatial library with support for this protocol you may use pygeoif as a starting point and build your functionality on top of it. It has no requirements outside the Python standard library and is therefore easy to integrate into your project. It is tested on `CPython `_ and `PyPy `_, but it should work on alternative Python implementations (that implement the language specification *>=3.8*) as well. You may think of pygeoif as a 'shapely ultralight' which lets you construct geometries and perform **very** basic operations like reading and writing geometries from/to WKT, constructing line strings out of points, polygons from linear rings, multi polygons from polygons, etc. It was inspired by shapely and implements the geometries in a way that when you are familiar with pygeoif, you will feel right at home with shapely or the other way round. It provides Hypothesis strategies for all geometries for property based testing with Hypothesis_. It was written to provide clean and python only geometries for fastkml_ .. image:: https://readthedocs.org/projects/pygeoif/badge/?version=latest :target: https://pygeoif.readthedocs.io/en/latest/?badge=latest :alt: Documentation .. image:: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml/badge.svg?branch=main :target: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml :alt: GitHub Actions .. image:: https://codecov.io/gh/cleder/pygeoif/branch/main/graph/badge.svg?token=2EfiwBXs9X :target: https://codecov.io/gh/cleder/pygeoif :alt: Codecov .. image:: https://img.shields.io/badge/property_based_tests-hypothesis-green :target: https://hypothesis.works :alt: Hypothesis .. image:: https://img.shields.io/badge/code_style-black-000000.svg :target: https://github.com/psf/ :alt: Black .. image:: https://img.shields.io/badge/type_checker-mypy-blue :target: http://mypy-lang.org/ :alt: Mypy .. image:: https://www.openhub.net/p/pygeoif/widgets/project_thin_badge.gif :target: https://www.openhub.net/p/pygeoif/ :alt: Openhub .. image:: https://www.codefactor.io/repository/github/cleder/pygeoif/badge/main :target: https://www.codefactor.io/repository/github/cleder/pygeoif/overview/main :alt: CodeFactor .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit :target: https://github.com/pre-commit/pre-commit :alt: pre-commit .. image:: https://img.shields.io/pypi/pyversions/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/implementation/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python implementations .. image:: https://img.shields.io/pypi/v/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: License .. image:: https://img.shields.io/pypi/dm/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Downloads Installation ------------ You can install PyGeoIf from pypi using pip:: pip install pygeoif Example ======== >>> from pygeoif import geometry >>> p = geometry.Point(1,1) >>> p.__geo_interface__ {'type': 'Point', 'bbox': (1, 1, 1, 1), 'coordinates': (1, 1)} >>> print(p) POINT (1 1) >>> p Point(1, 1) >>> l = geometry.LineString([(0.0, 0.0), (1.0, 1.0)]) >>> l.bounds (0.0, 0.0, 1.0, 1.0) >>> print(l) LINESTRING (0.0 0.0, 1.0 1.0) You find more examples in the `tests `_ directory which cover every aspect of pygeoif or in fastkml_. Classes ======== All classes implement the attribute: * ``__geo_interface__``: as discussed above, an interface to GeoJSON_. All geometry classes implement the attributes: * ``geom_type``: Returns a string specifying the Geometry Type of the object * ``bounds``: Returns a (minx, miny, maxx, maxy) tuple that bounds the object. * ``wkt``: Returns the 'Well Known Text' representation of the object For two-dimensional geometries the following methods are implemented: * ``convex_hull``: Returns a representation of the smallest convex Polygon containing all the points in the object unless the number of points in the object is less than three. For two points, the convex hull collapses to a LineString; for 1, a Point. For three dimensional objects only their projection in the xy plane is taken into consideration. Empty objects without coordinates return ``None`` for the convex_hull. Point ----- A zero dimensional geometry A point has zero length and zero area. A point cannot be empty. Attributes ~~~~~~~~~~~ x, y, z : float Coordinate values Example ~~~~~~~~ >>> from pygeoif import Point >>> p = Point(1.0, -1.0) >>> print(p) POINT (1.0 -1.0) >>> p.y -1.0 >>> p.x 1.0 LineString ----------- A one-dimensional figure comprising one or more line segments A LineString has non-zero length and zero area. It may approximate a curve and need not be straight. Unlike a LinearRing, a LineString is not closed. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of Points LinearRing ----------- A closed one-dimensional geometry comprising one or more line segments A LinearRing that crosses itself or touches itself at a single point is invalid and operations on it may fail. A LinearRing is self closing. Polygon -------- A two-dimensional figure bounded by a linear ring A polygon has a non-zero area. It may have one or more negative-space "holes" which are also bounded by linear rings. If any rings cross each other, the geometry is invalid and operations on it may fail. Attributes ~~~~~~~~~~~ exterior : LinearRing The ring which bounds the positive space of the polygon. interiors : sequence A sequence of rings which bound all existing holes. maybe_valid: boolean When a polygon has obvious problems such as self crossing lines or holes that are outside the exterior bounds this will return False. Even if this returns True the geometry may still be invalid, but if this returns False you do have a problem. MultiPoint ---------- A collection of one or more points. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of Points. MultiLineString ---------------- A collection of one or more line strings. A MultiLineString has non-zero length and zero area. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of LineStrings MultiPolygon ------------- A collection of one or more polygons. Attributes ~~~~~~~~~~~~~ geoms : sequence A sequence of `Polygon` instances GeometryCollection ------------------- A heterogenous collection of geometries (Points, LineStrings, LinearRings and Polygons). Attributes ~~~~~~~~~~~ geoms : sequence A sequence of geometry instances Please note: ``GEOMETRYCOLLECTION`` isn't supported by the Shapefile or GeoJSON_ format. And this sub-class isn't generally supported by ordinary GIS sw (viewers and so on). So it's very rarely used in the real GIS professional world. Example ~~~~~~~~ >>> from pygeoif import geometry >>> p = geometry.Point(1.0, -1.0) >>> p2 = geometry.Point(1.0, -1.0) >>> geoms = [p, p2] >>> c = geometry.GeometryCollection(geoms) >>> [geom for geom in geoms] [Point(1.0, -1.0), Point(1.0, -1.0)] Feature ------- Aggregates a geometry instance with associated user-defined properties. Attributes ~~~~~~~~~~~ geometry : object A geometry instance properties : dict A dictionary linking field keys with values associated with with geometry instance Example ~~~~~~~~ >>> from pygeoif import Point, Feature >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = Feature(p, props) >>> a.properties {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a.properties['Name'] 'Sample Point' FeatureCollection ----------------- A heterogenous collection of Features Attributes ~~~~~~~~~~~ features: sequence A sequence of feature instances Example ~~~~~~~~ >>> from pygeoif import Point, Feature, FeatureCollection >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = Feature(p, props) >>> p2 = Point(1.0, -1.0) >>> props2 = {'Name': 'Sample Point2', 'Other': 'Other Data2'} >>> b = Feature(p2, props2) >>> features = [a, b] >>> c = FeatureCollection(features) >>> [feature for feature in c] [Feature(Point(1.0, -1.0), {'Name': 'Sample Point', 'Other': 'Other Data'},...] Functions ========= shape -------- Create a pygeoif feature from an object that provides the ``__geo_interface__`` or any GeoJSON_ compatible dictionary. >>> from shapely.geometry import Point >>> from pygeoif import geometry, shape >>> shape(Point(0,0)) Point(0.0, 0.0) from_wkt --------- Create a geometry from its WKT representation >>> from pygeoif import from_wkt >>> p = from_wkt('POINT (0 1)') >>> print(p) POINT (0.0 1.0) signed_area ------------ Return the signed area enclosed by a ring. A value >= 0 indicates a counter-clockwise oriented ring. orient ------- Returns a copy of a polygon with exteriors and interiors in the right orientation. if ccw is True than the exterior will be in counterclockwise orientation and the interiors will be in clockwise orientation, or the other way round when ccw is False. box --- Return a rectangular polygon with configurable normal vector. mapping ------- Return the ``__geo_interface__`` dictionary. Development =========== Clone this repository, create a virtualenv with Python 3.8 or later with ``python3 -m venv .venv`` and activate it with ``source .venv/bin/activate``. Then install the requirements with ``pip install -e ".[dev]"``. 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 Testing ------- Run the unit and static tests with:: pytest tests pytest --doctest-glob="README.rst" black pygeoif ruff pygeoif flake8 pygeoif mypy pygeoif Acknowledgments ================ The tests were improved with mutmut_ which discovered some nasty edge cases. .. _mutmut: https://github.com/boxed/mutmut .. _GeoJSON: https://geojson.org/ .. _fastkml: http://pypi.python.org/pypi/fastkml/ .. _Hypothesis: https://hypothesis.works ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1711389497.4274578 pygeoif-1.4.0/pygeoif/0000755000175100001770000000000014600335471014230 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/__init__.py0000644000175100001770000000334114600335465016345 0ustar00runnerdocker# # Copyright (C) 2012 - 2023 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 Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # """PyGeoIf provides a GeoJSON-like protocol for geo-spatial (GIS) vector data.""" from pygeoif.about import __version__ # noqa: F401 from pygeoif.factories import from_wkt from pygeoif.factories import mapping from pygeoif.factories import orient from pygeoif.factories import shape from pygeoif.feature import Feature from pygeoif.feature import FeatureCollection from pygeoif.geometry import GeometryCollection from pygeoif.geometry import LinearRing from pygeoif.geometry import LineString from pygeoif.geometry import MultiLineString from pygeoif.geometry import MultiPoint from pygeoif.geometry import MultiPolygon from pygeoif.geometry import Point from pygeoif.geometry import Polygon __all__ = [ "Feature", "FeatureCollection", "GeometryCollection", "LineString", "LinearRing", "MultiLineString", "MultiPoint", "MultiPolygon", "Point", "Polygon", "from_wkt", "mapping", "orient", "shape", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/about.py0000644000175100001770000000017714600335465015724 0ustar00runnerdocker""" About pygeoif. The only purpose of this module is to provide a version number for the package. """ __version__ = "1.4.0" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/exceptions.py0000644000175100001770000000222214600335465016764 0ustar00runnerdocker# # 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 Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Exceptions for pygeoif.""" class DimensionError(ValueError): """Geometries must have 2 or 3 dimensions.""" class WKTParserError(AttributeError): """WKT not supported or cannot be parsed.""" class InvalidGeometryError(ValueError): """Geometry is not valid.""" __all__ = [ "DimensionError", "InvalidGeometryError", "WKTParserError", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/factories.py0000644000175100001770000002735414600335465016577 0ustar00runnerdocker# # 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 Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Geometry Factories.""" import re from typing import List from typing import Optional from typing import Pattern from typing import Tuple from typing import Union from typing import cast from pygeoif.exceptions import WKTParserError from pygeoif.functions import move_geo_interface from pygeoif.functions import signed_area from pygeoif.geometry import Geometry from pygeoif.geometry import GeometryCollection from pygeoif.geometry import LinearRing from pygeoif.geometry import LineString from pygeoif.geometry import MultiLineString from pygeoif.geometry import MultiPoint from pygeoif.geometry import MultiPolygon from pygeoif.geometry import Point from pygeoif.geometry import Polygon from pygeoif.types import GeoCollectionInterface from pygeoif.types import GeoCollectionType from pygeoif.types import GeoInterface from pygeoif.types import GeoType from pygeoif.types import Interiors from pygeoif.types import LineType from pygeoif.types import PointType from pygeoif.types import PolygonType wkt_regex: Pattern[str] = re.compile( pattern=( r"^(SRID=(?P\d+);)?" "(?P" "(?PPOINT|LINESTRING|LINEARRING|POLYGON|" "MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|" "GEOMETRYCOLLECTION)" r"[ACEGIMLONPSRUTYZ\d,\.\-\(\) ]+)$" ), flags=re.I, ) gcre: Pattern[str] = re.compile( r"POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON", ) outer: Pattern[str] = re.compile(r"\((.+)\)") inner: Pattern[str] = re.compile(r"\([^)]*\)") mpre: Pattern[str] = re.compile(r"\(\((.+?)\)\)") def get_oriented_ring(ring: LineType, ccw: bool) -> LineType: # noqa: FBT001 s = 1.0 if ccw else -1.0 return ring if signed_area(ring) / s >= 0 else ring[::-1] def orient(polygon: Polygon, ccw: bool = True) -> Polygon: # noqa: FBT001, FBT002 """ Return a polygon with exteriors and interiors in the right orientation. if ccw is True than the exterior will be in counterclockwise orientation and the interiors will be in clockwise orientation, or the other way round when ccw is False. """ shell = get_oriented_ring(polygon.exterior.coords, ccw) ccw = not ccw # flip orientation for holes holes = [get_oriented_ring(ring.coords, ccw) for ring in polygon.interiors] return Polygon(shell=shell, holes=holes) def box( minx: float, miny: float, maxx: float, maxy: float, ccw: bool = True, # noqa: FBT001, FBT002 ) -> Polygon: """Return a rectangular polygon with configurable normal vector.""" coords = [(maxx, miny), (maxx, maxy), (minx, maxy), (minx, miny)] if not ccw: coords = coords[::-1] return Polygon(coords) def shape( context: Union[ GeoType, GeoCollectionType, GeoInterface, GeoCollectionInterface, ], ) -> Union[Geometry, GeometryCollection]: """ Return a new geometry with coordinates *copied* from the context. Changes to the original context will not be reflected in the geometry object. Parameters ---------- context : a GeoJSON-like dict, which provides a "type" member describing the type of the geometry and "coordinates" member providing a list of coordinates, or an object which implements __geo_interface__. Returns ------- Geometry object Example ------- Create a Point from GeoJSON, and then create a copy using __geo_interface__. >>> context = {'type': 'Point', 'coordinates': [0, 1]} >>> geom = shape(context) >>> geom.geom_type == 'Point' True >>> geom.wkt 'POINT (0 1)' >>> geom2 = shape(geom) >>> geom == geom2 True """ type_map = { "Point": Point, "LineString": LineString, "LinearRing": LinearRing, "Polygon": Polygon, "MultiPoint": MultiPoint, "MultiLineString": MultiLineString, "MultiPolygon": MultiPolygon, } geometry = context if isinstance(context, dict) else mapping(context) if not geometry: msg = ( # type: ignore [unreachable] "Object does not implement __geo_interface__" ) raise TypeError(msg) constructor = type_map.get(geometry["type"]) if constructor: return constructor._from_dict( # type: ignore [attr-defined, no-any-return] geometry, ) if geometry["type"] == "GeometryCollection": geometries = [ shape(fi) for fi in geometry["geometries"] # type: ignore [typeddict-item] ] return GeometryCollection(geometries) msg = f"[{geometry['type']} is not implemented" raise NotImplementedError(msg) def num(number: str) -> float: """ Return a float or integer from a string. Parameters ---------- number : str a string representing a number Returns ------- float or an integer if the string can be converted to an integer """ f = float(number) return int(f) if int(f) == f else f def _point_from_wkt_coordinates(coordinates: str) -> Point: coords = [num(c) for c in coordinates.split()] return Point(*coords) def _line_from_wkt_coordinates(coordinates: str) -> LineString: coords = coordinates.split(",") return LineString( cast( LineType, # [tuple(num(c) for c in coord.split()) for coord in coords], ), ) def _ring_from_wkt_coordinates(coordinates: str) -> LinearRing: coords = coordinates.split(",") return LinearRing( cast( LineType, [tuple(num(c) for c in coord.split()) for coord in coords], ), ) def _shell_holes_from_wkt_coords( coords: List[str], ) -> Tuple[LineType, Interiors]: """Extract shell and holes from polygon wkt coordinates.""" exterior: LineType = cast( LineType, [tuple(num(c) for c in coord.split()) for coord in coords[0]], ) if len(coords) > 1: # we have a polygon with holes interiors = [ cast( LineType, [ cast(PointType, tuple(num(c) for c in coord.split())) for coord in ext ], ) for ext in coords[1:] ] else: interiors = None return exterior, interiors def _polygon_from_wkt_coordinates(coordinates: str) -> Polygon: coords = [ interior.strip("()").split(",") for interior in inner.findall(coordinates) ] interior, exteriors = _shell_holes_from_wkt_coords(coords) return Polygon( shell=interior, holes=exteriors, ) def _multipoint_from_wkt_coordinates(coordinates: str) -> MultiPoint: coords = [coord.strip().strip("()") for coord in coordinates.split(",")] return MultiPoint( [cast(PointType, tuple(num(c) for c in coord.split())) for coord in coords], ) def _multiline_from_wkt_coordinates(coordinates: str) -> MultiLineString: coords = [ cast( LineType, [ tuple(num(c) for c in coord.split()) for coord in lines.strip("()").split(",") ], ) for lines in inner.findall(coordinates) ] return MultiLineString(coords) def _multipolygon_from_wkt_coordinates(coordinates: str) -> MultiPolygon: polygons: List[PolygonType] = [] m = mpre.split(coordinates) for polygon in m: if not polygon.strip(", "): continue coords = [ interior.strip("()").split(",") for interior in inner.findall(f"({polygon})") ] interior, exteriors = _shell_holes_from_wkt_coords(coords) if exteriors: polygons.append(cast(PolygonType, [interior, exteriors])) else: polygons.append(cast(PolygonType, [interior])) return MultiPolygon(polygons) def _multigeometry_from_wkt_coordinates(coordinates: str) -> GeometryCollection: gc_types = gcre.findall(coordinates) gc_coords = gcre.split(coordinates)[1:] geometries: List[Geometry] = [] for gc_type, gc_coord in zip(gc_types, gc_coords): gc_wkt = gc_type + gc_coord[: gc_coord.rfind(")") + 1] geometries.append(cast(Geometry, from_wkt(gc_wkt))) return GeometryCollection(geometries) def from_wkt(geo_str: str) -> Optional[Union[Geometry, GeometryCollection]]: """Create a geometry from its WKT representation.""" type_map = { "POINT": _point_from_wkt_coordinates, "LINESTRING": _line_from_wkt_coordinates, "LINEARRING": _ring_from_wkt_coordinates, "POLYGON": _polygon_from_wkt_coordinates, "MULTIPOINT": _multipoint_from_wkt_coordinates, "MULTILINESTRING": _multiline_from_wkt_coordinates, "MULTIPOLYGON": _multipolygon_from_wkt_coordinates, "GEOMETRYCOLLECTION": _multigeometry_from_wkt_coordinates, } wkt = geo_str.upper().strip() wkt = " ".join(line.strip() for line in wkt.splitlines()) try: wkt = wkt_regex.match(wkt).group("wkt") # type: ignore [union-attr] geometry_type = wkt_regex.match(wkt).group("type") # type: ignore [union-attr] outerstr = outer.search(wkt) coordinates = outerstr.group(1) # type: ignore [union-attr] except AttributeError as exc: msg = f"Cannot parse {wkt}" raise WKTParserError(msg) from exc constructor = type_map[geometry_type] try: return constructor(coordinates) # type: ignore [return-value] except TypeError as exc: msg = f"Cannot parse {wkt}" raise WKTParserError(msg) from exc def mapping( ob: Union[GeoType, GeoCollectionType], ) -> Union[GeoCollectionInterface, GeoInterface]: """ Return a GeoJSON-like mapping. Parameters ---------- ob : An object which implements __geo_interface__. Returns ------- dict Example ------- >>> pt = Point(0, 0) >>> mapping(pt) {'type': 'Point', 'bbox': (0, 0, 0, 0), 'coordinates': (0, 0)} """ return ob.__geo_interface__ def force_2d( context: Union[GeoType, GeoCollectionType], ) -> Union[Geometry, GeometryCollection]: """ Force the dimensionality of a geometry to 2D. >>> force_2d(Point(0, 0, 1)) Point(0, 0) >>> force_2d(Point(0, 0)) Point(0, 0) >>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)])) LineString(((0, 0), (0, 1), (1, 1))) """ geometry = mapping(context) return shape(move_geo_interface(geometry, (0, 0))) def force_3d( context: Union[GeoType, GeoCollectionType], z: float = 0, ) -> Union[Geometry, GeometryCollection]: """ Force the dimensionality of a geometry to 3D. >>> force_3d(Point(0, 0)) Point(0, 0, 0) >>> force_3d(Point(0, 0), 1) Point(0, 0, 1) >>> force_3d(Point(0, 0, 0)) Point(0, 0, 0) >>> force_3d(LineString([(0, 0), (0, 1), (1, 1)])) LineString(((0, 0, 0), (0, 1, 0), (1, 1, 0))) """ geometry = mapping(context) return shape(move_geo_interface(geometry, (0, 0, z))) __all__ = [ "force_2d", "force_3d", "box", "from_wkt", "mapping", "orient", "shape", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/feature.py0000644000175100001770000001773414600335465016254 0ustar00runnerdocker# # 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 Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # # file deepcode ignore inconsistent~equality: Python 3 only """Features.""" from typing import Any from typing import Dict from typing import Generator from typing import Iterator from typing import Optional from typing import Sequence from typing import Union from typing import cast from pygeoif.functions import compare_coordinates from pygeoif.geometry import Geometry from pygeoif.types import Bounds from pygeoif.types import GeoFeatureCollectionInterface from pygeoif.types import GeoFeatureInterface def feature_geo_interface_equals( my_interface: GeoFeatureInterface, other_interface: GeoFeatureInterface, ) -> bool: """Check if my interface is the same as the other interface.""" return all( [ my_interface.get("id") == other_interface.get("id"), my_interface["type"] == other_interface.get("type"), my_interface["properties"] == other_interface.get("properties"), my_interface["geometry"]["type"] == other_interface["geometry"].get("type"), compare_coordinates( coords=my_interface["geometry"]["coordinates"], other=other_interface["geometry"].get( # type: ignore [arg-type] "coordinates", ), ), ], ) class Feature: """ Aggregates a geometry instance with associated user-defined properties. Attributes: ---------- geometry : object A geometry instance properties : dict A dictionary linking field keys with values associated with geometry instance Example: ------- >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = Feature(p, props) >>> a.properties {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a.properties['Name'] 'Sample Point' """ def __init__( self, geometry: Geometry, properties: Optional[Dict[str, Any]] = None, feature_id: Optional[Union[str, int]] = None, ) -> None: """Initialize the feature.""" self._geometry = geometry self._properties = properties or {} self._feature_id = feature_id def __eq__(self, other: object) -> bool: """Check if the geointerfaces are equal.""" try: if not other.__geo_interface__.get( # type: ignore [attr-defined] "geometry", ): return False except AttributeError: return False return feature_geo_interface_equals( my_interface=self.__geo_interface__, other_interface=other.__geo_interface__, # type: ignore [attr-defined] ) def __repr__(self) -> str: """Return the representation.""" return ( f"{self.__class__.__name__}({self._geometry!r}," f" {self._properties}, {self._feature_id!r})" ) @property def id(self) -> Optional[Union[str, int]]: """Return the id of the feature.""" return self._feature_id @property def geometry(self) -> Geometry: """Return the geometry of the feature.""" return self._geometry @property def properties(self) -> Dict[str, Any]: """Return a dictionary of properties.""" return self._properties @property def __geo_interface__(self) -> GeoFeatureInterface: """Return the GeoInterface of the geometry with properties.""" geo_interface: GeoFeatureInterface = { "type": "Feature", "bbox": cast(Bounds, self._geometry.bounds), "geometry": self._geometry.__geo_interface__, "properties": self._properties, } if self._feature_id is not None: geo_interface["id"] = self._feature_id return geo_interface class FeatureCollection: """ A heterogenous collection of Features. Attributes: ---------- features : sequence A sequence of feature instances Example: ------- >>> from pygeoif import geometry >>> p = geometry.Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = geometry.Feature(p, props) >>> p2 = geometry.Point(1.0, -1.0) >>> props2 = {'Name': 'Sample Point2', 'Other': 'Other Data2'} >>> b = geometry.Feature(p2, props2) >>> features = [a, b] >>> c = geometry.FeatureCollection(features) >>> c.__geo_interface__ {'type': 'FeatureCollection', 'features': [{'geometry': {'type': 'Point', 'coordinates': (1.0, -1.0)}, 'type': 'Feature', 'properties': {'Other': 'Other Data', 'Name': 'Sample Point'}}, {'geometry': {'type': 'Point', 'coordinates': (1.0, -1.0)}, 'type': 'Feature', 'properties': {'Other': 'Other Data2', 'Name': 'Sample Point2'}}]} """ def __init__(self, features: Sequence[Feature]) -> None: """Initialize the feature.""" self._features = tuple(features) def __eq__(self, other: object) -> bool: """Check if the geointerfaces are equal.""" return self._check_interface(other) and all( ( feature_geo_interface_equals(my_interface=mine, other_interface=other) for mine, other in zip( self.__geo_interface__["features"], other.__geo_interface__["features"], # type: ignore [attr-defined] ) ), ) def __len__(self) -> int: """Return the umber of features in this collection.""" return len(self._features) def __iter__(self) -> Iterator[Feature]: """Iterate over the features of the collection.""" return iter(self._features) def __repr__(self) -> str: """Return the representation.""" return f"{self.__class__.__name__}({self._features!r})" @property def features(self) -> Generator[Feature, None, None]: """Iterate over the features of the collection.""" yield from self._features @property def bounds(self) -> Bounds: """Return the X-Y bounding box.""" geom_bounds = list( zip(*(feature.geometry.bounds for feature in self._features)), ) return ( min(geom_bounds[0]), min(geom_bounds[1]), max(geom_bounds[2]), max(geom_bounds[3]), ) @property def __geo_interface__(self) -> GeoFeatureCollectionInterface: """Return the GeoInterface of the feature.""" return { "type": "FeatureCollection", "bbox": self.bounds, "features": tuple(feature.__geo_interface__ for feature in self._features), } def _check_interface(self, other: object) -> bool: try: return self.__geo_interface__[ "type" ] == other.__geo_interface__.get( # type: ignore [attr-defined] "type", ) and len( self.__geo_interface__["features"], ) == len( other.__geo_interface__.get( # type: ignore [attr-defined] "features", [], ), ) except AttributeError: return False __all__ = [ "Feature", "FeatureCollection", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/functions.py0000644000175100001770000002013514600335465016616 0ustar00runnerdocker# # Copyright (C) 2012 - 2024 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 Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # """Functions for geometries.""" import math from itertools import groupby from itertools import zip_longest from typing import Iterable from typing import List from typing import Tuple from typing import Union from typing import cast from pygeoif.types import CoordinatesType from pygeoif.types import GeoCollectionInterface from pygeoif.types import GeoInterface from pygeoif.types import LineType from pygeoif.types import MultiCoordinatesType from pygeoif.types import Point2D from pygeoif.types import PointType def signed_area(coords: LineType) -> float: """ Return the signed area enclosed by a ring. Linear time algorithm: http://www.cgafaq.info/wiki/Polygon_Area. A value >= 0 indicates a counter-clockwise oriented ring. """ xs, ys = map(list, zip(*(coord[:2] for coord in coords))) xs.append(xs[1]) # pragma: no mutate ys.append(ys[1]) # pragma: no mutate return cast( float, sum(xs[i] * (ys[i + 1] - ys[i - 1]) for i in range(1, len(coords))) / 2.0, ) def centroid(coords: LineType) -> Tuple[Point2D, float]: """Calculate the coordinates of the centroid and the area of a LineString.""" ans: List[float] = [0, 0] n = len(coords) signed_area = 0.0 # For all vertices for i, coord in enumerate(coords): next_coord = coords[(i + 1) % n] # Calculate area using shoelace formula area = (coord[0] * next_coord[1]) - (next_coord[0] * coord[1]) signed_area += area # Calculate coordinates of centroid of polygon ans[0] += (coord[0] + next_coord[0]) * area ans[1] += (coord[1] + next_coord[1]) * area ans[0] = (ans[0]) / (3 * signed_area) ans[1] = (ans[1]) / (3 * signed_area) return cast(Point2D, tuple(ans)), signed_area / 2.0 def dedupe(coords: LineType) -> LineType: """Remove duplicate Points from a LineString.""" return cast(LineType, tuple(coord for coord, _count in groupby(coords))) def _orientation(p: Point2D, q: Point2D, r: Point2D) -> float: """ Calculate orientation of three points (p, q, r). Returns ------- negative if counterclockwise 0 if colinear positive if clockwise """ return (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]) def _hull(points: Iterable[Point2D]) -> List[Point2D]: """Construct the upper/lower hull of a set of points.""" stack: List[Point2D] = [] for p in points: while ( len(stack) >= 2 # noqa: PLR2004 and _orientation(stack[-2], stack[-1], p) >= 0 ): stack.pop() stack.append(p) return stack def convex_hull(points: Iterable[Point2D]) -> LineType: """ Return the convex hull of a set of points using Andrew's monotone chain algorithm. Args: ---- points (Iterable[Point2D]): A collection of 2D points. Returns: ------- LineType: The convex hull, represented as a list of points. """ points = sorted(set(points)) # No points, a single point or a line between two points if len(points) <= 2: # noqa: PLR2004 return points # Construct the upper and lower hulls upper = _hull(points) lower = _hull(reversed(points)) if len(lower) == len(upper) == 2 and set(lower) == set(upper): # noqa: PLR2004 # all points are in a straight line return upper # Remove duplicate points (at the end of upper and beginning of lower) return dedupe(upper + lower) def compare_coordinates( coords: Union[float, CoordinatesType, MultiCoordinatesType], other: Union[float, CoordinatesType, MultiCoordinatesType], ) -> bool: """Compare two coordinate sequences.""" try: return all( compare_coordinates(coords=c, other=o) for c, o in zip_longest( coords, # type: ignore [arg-type] other, # type: ignore [arg-type] fillvalue=math.nan, ) ) except TypeError: try: return math.isclose(a=cast(float, coords), b=cast(float, other)) except TypeError: return False def compare_geo_interface( first: Union[GeoInterface, GeoCollectionInterface], second: Union[GeoInterface, GeoCollectionInterface], ) -> bool: """Compare two geo interfaces.""" try: if first["type"] != second["type"]: return False if first["type"] == "GeometryCollection": return all( compare_geo_interface(first=g1, second=g2) # type: ignore [arg-type] for g1, g2 in zip_longest( first["geometries"], # type: ignore [typeddict-item] second["geometries"], # type: ignore [typeddict-item] fillvalue={"type": None, "coordinates": ()}, ) ) return compare_coordinates( coords=first["coordinates"], # type: ignore [typeddict-item] other=second["coordinates"], # type: ignore [typeddict-item] ) except KeyError: return False def move_coordinate( coordinate: PointType, move_by: PointType, ) -> PointType: """ Move the coordinate by the given vector. This forcefully changes the dimensions of the coordinate to match the latter. >>> move_coordinate((0, 0), (-1, 1)) (-1, 1) >>> move_coordinate((0, 0, 0), (-1, 1)) (-1, 1) >>> move_coordinate((0, 0), (-1, 1, 0)) (-1, 1, 0) """ if len(coordinate) < len(move_by): return cast( PointType, tuple(c + m for c, m in zip_longest(coordinate, move_by, fillvalue=0)), ) return cast(PointType, tuple(c + m for c, m in zip(coordinate, move_by))) def move_coordinates( coordinates: CoordinatesType, move_by: PointType, ) -> CoordinatesType: """ Move the coordinates recursively by the given vector. This forcefully changes the dimension of each of the coordinate to match the dimensionality of the vector. >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1)) ((-1, 1), (-2, 2)) >>> move_coordinates(((0, 0, 0), (-1, 1, 0)), (-1, 1)) ((-1, 1), (-2, 2)) >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1, 0)) ((-1, 1, 0), (-2, 2, 0)) """ if isinstance(coordinates[0], (int, float)): return move_coordinate(cast(PointType, coordinates), move_by) return cast( CoordinatesType, tuple(move_coordinates(cast(CoordinatesType, c), move_by) for c in coordinates), ) def move_geo_interface( interface: Union[GeoInterface, GeoCollectionInterface], move_by: PointType, ) -> Union[GeoInterface, GeoCollectionInterface]: """Move the coordinates of the geo interface by the given vector.""" if interface["type"] == "GeometryCollection": return { "type": "GeometryCollection", "geometries": tuple( move_geo_interface(g, move_by) for g in interface["geometries"] # type: ignore [typeddict-item] ), } return { "type": interface["type"], "coordinates": move_coordinates( interface["coordinates"], # type: ignore [typeddict-item, arg-type] move_by, ), } __all__ = [ "centroid", "compare_coordinates", "compare_geo_interface", "convex_hull", "dedupe", "move_coordinate", "move_coordinates", "signed_area", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/geometry.py0000644000175100001770000007762114600335465016455 0ustar00runnerdocker# # Copyright (C) 2012 -2024 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 Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # # file deepcode ignore inconsistent~equality: Python 3 only """Geometries in pure Python.""" import math import warnings from itertools import chain from typing import Any from typing import Hashable from typing import Iterable from typing import Iterator from typing import NoReturn from typing import Optional from typing import Sequence from typing import Tuple from typing import Union from typing import cast from pygeoif.exceptions import DimensionError from pygeoif.functions import centroid from pygeoif.functions import compare_coordinates from pygeoif.functions import compare_geo_interface from pygeoif.functions import convex_hull from pygeoif.functions import dedupe from pygeoif.functions import signed_area from pygeoif.types import Bounds from pygeoif.types import GeoCollectionInterface from pygeoif.types import GeoInterface from pygeoif.types import GeoType from pygeoif.types import LineType from pygeoif.types import Point2D from pygeoif.types import PointType from pygeoif.types import PolygonType class _Geometry: """Base Class for geometry objects.""" __slots__ = ("_geoms",) _geoms: Hashable def __setattr__(self, *args: Any) -> NoReturn: # noqa: ANN401 msg = f"Attributes of {self.__class__.__name__} cannot be changed" raise AttributeError( msg, ) def __delattr__(self, *args: Any) -> NoReturn: # noqa: ANN401 msg = f"Attributes of {self.__class__.__name__} cannot be deleted" raise AttributeError( msg, ) def __str__(self) -> str: return self.wkt def __eq__(self, other: object) -> bool: """ Check if the geometry objects have the same coordinates and type. Empty geometries are always considered as not equal. """ try: return all( ( not self.is_empty, self.__geo_interface__["type"] == other.__geo_interface__.get( # type: ignore [attr-defined] "type", ), compare_coordinates( coords=self.__geo_interface__["coordinates"], other=other.__geo_interface__.get( # type: ignore [attr-defined] "coordinates", ), ), ), ) except AttributeError: return False def __bool__(self) -> bool: return self.is_empty is False @property def bounds(self) -> Union[Bounds, Tuple[()]]: """ Return minimum bounding region (min x, min y, max x, max y). An empty geometry returns an empty tuple. """ return () if self.is_empty else self._get_bounds() @property def convex_hull(self) -> Optional[Union["Point", "LineString", "Polygon"]]: """ Return the Convex Hull. Returns a representation of the smallest convex Polygon containing all the points in the object unless the number of points in the object is fewer than three. For two points, the convex hull collapses to a LineString; for 1, to a Point. """ if self.has_z: warnings.warn( "The convex Hull will only return the projection to" " 2 dimensions xy coordinates", stacklevel=2, ) hull = convex_hull(self._prepare_hull()) if len(hull) == 0: return None if len(hull) == 1: return Point(*hull[0]) return LineString(hull) if len(hull) == 2 else Polygon(hull) # noqa: PLR2004 @property def geom_type(self) -> str: """Return a string specifying the Geometry Type of the object.""" return self.__class__.__name__ @property def has_z(self) -> Optional[bool]: """ Return True if the geometry's coordinate sequence(s) have z values. Return None if the geometry is empty. """ msg = "Must be implemented by subclass" raise NotImplementedError(msg) @property def is_empty(self) -> bool: """Return if this geometry is empty.""" msg = "Must be implemented by subclass" raise NotImplementedError(msg) @property def wkt(self) -> str: """Return the Well Known Text representation of the object.""" if self.is_empty: return f"{self._wkt_type} EMPTY" return f"{self._wkt_type}{self._wkt_inset}({self._wkt_coords})" @property def __geo_interface__(self) -> GeoInterface: if self.is_empty: msg = "Empty Geometry" raise AttributeError(msg) return { "type": self.geom_type, "bbox": cast(Bounds, self.bounds), "coordinates": (), } @property def _wkt_coords(self) -> str: msg = "Must be implemented by subclass" raise NotImplementedError(msg) @property def _wkt_inset(self) -> str: """Return Z for 3 dimensional geometry or an empty string for 2 dimensions.""" return " Z " if self.has_z else " " @property def _wkt_type(self) -> str: """Return the WKT name of the geometry.""" return self.__class__.__name__.upper() @classmethod def _check_dict(cls, geo_interface: GeoInterface) -> None: if geo_interface["type"] != cls.__name__: msg = f"You cannot assign {geo_interface['type']} to {cls.__name__}" raise ValueError( msg, ) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "_Geometry": cls._check_dict(geo_interface) msg = "Must be implemented by subclass" raise NotImplementedError(msg) @classmethod def _from_interface(cls, obj: GeoType) -> "_Geometry": return cls._from_dict(obj.__geo_interface__) def _prepare_hull(self) -> Iterable[Point2D]: msg = "Must be implemented by subclass" raise NotImplementedError(msg) def _get_bounds(self) -> Bounds: msg = "Must be implemented by subclass" raise NotImplementedError(msg) class Point(_Geometry): """ A zero dimensional geometry. A point has zero length and zero area. Attributes: ---------- x, y, z : float Coordinate values Example: ------- >>> p = Point(1.0, -1.0) >>> print p POINT (1.0 -1.0) >>> p.y -1.0 >>> p.x 1.0 """ _geoms: PointType def __init__(self, x: float, y: float, z: Optional[float] = None) -> None: """ Initialize a Point. Parameters ---------- 2 or 3 coordinate parameters: x, y, [z] : float Easting, northing, and elevation. """ object.__setattr__( self, "_geoms", cast( PointType, tuple( coordinate for coordinate in (x, y, z) if coordinate is not None and not math.isnan(coordinate) ), ), ) def __repr__(self) -> str: """Return the representation.""" return f"{self.geom_type}{self._geoms}" @property def is_empty(self) -> bool: """ Return if this geometry is empty. A Point is considered empty when it has fewer than 2 coordinates. """ return len(self._geoms) < 2 # noqa: PLR2004 @property def x(self) -> float: """Return x coordinate.""" return self._geoms[0] @property def y(self) -> float: """Return y coordinate.""" return self._geoms[1] @property def z(self) -> Optional[float]: """Return z coordinate.""" if self.has_z: return self._geoms[2] # type: ignore [misc] msg = f"The {self!r} geometry does not have z values" raise DimensionError(msg) @property def coords(self) -> Tuple[PointType]: """Return the geometry coordinates.""" return (self._geoms,) @property def has_z(self) -> bool: """Return True if the geometry's coordinate sequence(s) have z values.""" return len(self._geoms) == 3 # noqa: PLR2004 @property def _wkt_coords(self) -> str: return " ".join(str(coordinate) for coordinate in self._geoms) @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ geo_interface["coordinates"] = cast(PointType, tuple(self._geoms)) return geo_interface @classmethod def from_coordinates(cls, coordinates: Sequence[PointType]) -> "Point": """Construct a point from coordinates.""" return cls(*coordinates[0]) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "Point": cls._check_dict(geo_interface) return cls(*geo_interface["coordinates"]) def _get_bounds(self) -> Bounds: return self.x, self.y, self.x, self.y def _prepare_hull(self) -> Iterable[Point2D]: return ((self.x, self.y),) class LineString(_Geometry): """ A one-dimensional figure comprising one or more line segments. A LineString has non-zero length and zero area. It may approximate a curve and need not be straight. Unlike a LinearRing, a LineString is not closed. Attributes ---------- geoms : sequence A sequence of Points """ _geoms: Tuple[Point, ...] def __init__(self, coordinates: LineType) -> None: """ Initialize a Linestring. Parameters ---------- coordinates : sequence A sequence of (x, y [,z]) numeric coordinate pairs or triples Example ------- Create a line with two segments >>> a = LineString([(0, 0), (1, 0), (1, 1)]) """ object.__setattr__(self, "_geoms", self._set_geoms(coordinates)) def __repr__(self) -> str: """Return the representation.""" return f"{self.geom_type}({self.coords})" @property def geoms(self) -> Tuple[Point, ...]: """Return the underlying geometries.""" return self._geoms @property def coords(self) -> LineType: """Return the geometry coordinates.""" return cast(LineType, tuple(point.coords[0] for point in self.geoms)) @property def is_empty(self) -> bool: """ Return if this geometry is empty. A Linestring is considered empty when it has no points. """ return len(self._geoms) == 0 @property def has_z(self) -> Optional[bool]: """Return True if the geometry's coordinate sequence(s) have z values.""" return self._geoms[0].has_z if self.geoms else None @property def _wkt_coords(self) -> str: return ", ".join(point._wkt_coords for point in self.geoms) # noqa: SLF001 @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ geo_interface["coordinates"] = self.coords return geo_interface @classmethod def from_coordinates(cls, coordinates: LineType) -> "LineString": """Construct a linestring from coordinates.""" return cls(coordinates) @classmethod def from_points(cls, *args: Point) -> "LineString": """Create a linestring from points.""" return cls(cast(LineType, tuple(point.coords[0] for point in args))) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "LineString": cls._check_dict(geo_interface) return cls(cast(LineType, geo_interface["coordinates"])) @staticmethod def _set_geoms(coordinates: LineType) -> Tuple[Point, ...]: geoms = [] last_len = None for coord in dedupe(coordinates): if len(coord) != last_len and last_len is not None: msg = ( # type: ignore [unreachable] "All coordinates must have the same dimension" ) raise DimensionError( msg, ) last_len = len(coord) point = Point(*coord) if point: geoms.append(point) return tuple(geoms) def _get_bounds(self) -> Bounds: """Return the X-Y bounding box.""" xy = list(zip(*((p.x, p.y) for p in self._geoms))) return ( min(xy[0]), min(xy[1]), max(xy[0]), max(xy[1]), ) def _prepare_hull(self) -> Iterable[Point2D]: return ((pt.x, pt.y) for pt in self._geoms) class LinearRing(LineString): """ A closed one-dimensional geometry comprising one or more line segments. A LinearRing that crosses itself or touches itself at a single point is invalid and operations on it may fail. A Linear Ring is self closing """ def __init__(self, coordinates: LineType) -> None: """ Initialize a LinearRing. Args: ---- coordinates (Sequence): A sequence of (x, y [,z]) numeric coordinate pairs or triples """ super().__init__(coordinates) if not self.is_empty and self._geoms[0].coords != self._geoms[-1].coords: object.__setattr__(self, "_geoms", (*self._geoms, self._geoms[0])) @property def centroid(self) -> Optional[Point]: """Return the centroid of the ring.""" if self.has_z: msg = "Centeroid is only implemented for 2D coordinates" raise DimensionError(msg) try: cent, area = centroid(self.coords) except ZeroDivisionError: return None return ( Point(x=cent[0], y=cent[1]) if math.isclose(a=area, b=signed_area(self.coords)) else None ) @property def is_ccw(self) -> bool: """Return True if the ring is oriented counter clock-wise.""" return signed_area(self.coords) >= 0 class Polygon(_Geometry): """ A two-dimensional figure bounded by a linear ring. A polygon has a non-zero area. It may have one or more negative-space "holes" which are also bounded by linear rings. If any rings cross each other, the geometry is invalid and operations on it may fail. Attributes ---------- exterior : LinearRing The ring which bounds the positive space of the polygon. interiors : sequence A sequence of rings which bound all existing holes. """ _geoms: Tuple[LinearRing, ...] def __init__( self, shell: LineType, holes: Optional[Sequence[LineType]] = None, ) -> None: """ Initialize the polygon. Parameters ---------- shell : sequence A sequence of (x, y [,z]) numeric coordinate pairs or triples holes : sequence A sequence of objects which satisfy the same requirements as the shell parameters above Example ------- Create a square polygon with no holes >>> coords = ((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.)) >>> polygon = Polygon(coords) """ interiors = tuple(LinearRing(hole) for hole in holes) if holes else () exterior = LinearRing(shell) object.__setattr__(self, "_geoms", (exterior, interiors)) def __repr__(self) -> str: """Return the representation.""" return f"{self.geom_type}{self.coords}" @property def exterior(self) -> LinearRing: """Return the exterior Linear Ring of the polygon.""" return self._geoms[0] @property def interiors(self) -> Iterator[LinearRing]: """Interiors (Holes) of the polygon.""" yield from ( interior for interior in self._geoms[1] # type: ignore [attr-defined] if interior ) @property def is_empty(self) -> bool: """ Return if this geometry is empty. A polygon is empty when it does not have an exterior. """ return self._geoms[0].is_empty @property def coords(self) -> PolygonType: """ Return Coordinates of the Polygon. Note that this is not implemented in Shapely. """ if self._geoms[1]: return cast( PolygonType, ( self.exterior.coords, tuple(interior.coords for interior in self.interiors if interior), ), ) return cast(PolygonType, (self.exterior.coords,)) @property def has_z(self) -> Optional[bool]: """Return True if the geometry's coordinate sequence(s) have z values.""" return self._geoms[0].has_z @property def _wkt_coords(self) -> str: ec = self.exterior._wkt_coords # noqa: SLF001 ic = "".join( f",({interior._wkt_coords})" for interior in self.interiors # noqa: SLF001 ) return f"({ec}){ic}" @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ coords = (self.exterior.coords, *tuple(hole.coords for hole in self.interiors)) geo_interface["coordinates"] = coords return geo_interface @classmethod def from_coordinates(cls, coordinates: PolygonType) -> "Polygon": """Construct a linestring from coordinates.""" return cls(*coordinates) @classmethod def from_linear_rings(cls, shell: LinearRing, *args: LinearRing) -> "Polygon": """Construct a Polygon from linear rings.""" return cls( shell=shell.coords, holes=tuple(lr.coords for lr in args), ) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "Polygon": cls._check_dict(geo_interface) return cls( shell=cast(LineType, geo_interface["coordinates"][0]), holes=cast(Tuple[LineType], geo_interface["coordinates"][1:]), ) def _get_bounds(self) -> Bounds: return self.exterior._get_bounds() # noqa: SLF001 def _prepare_hull(self) -> Iterable[Point2D]: return self.exterior._prepare_hull() # noqa: SLF001 class _MultiGeometry(_Geometry): """ Heterogeneous collections of geometric objects. The collection may be homogeneous (MultiPoint etc.) or heterogeneous. """ @property def coords(self) -> NoReturn: """ Raise a NotImplementedError. Multi-part geometries do not provide a coordinate sequence. """ msg = "Multi-part geometries do not provide a coordinate sequence" raise NotImplementedError( msg, ) @property def has_z(self) -> Optional[bool]: """Return True if any geometry of the collection have z values.""" return any(geom.has_z for geom in self.geoms) if self._geoms else None @property def geoms(self) -> Iterator[_Geometry]: """Iterate over the geometries.""" yield from ( geom for geom in self._geoms # type: ignore [attr-defined] if not geom.is_empty ) @property def is_empty(self) -> bool: """Return if collection is not empty and all its member are not empty.""" return all(geom.is_empty for geom in self._geoms) # type: ignore [attr-defined] def _get_bounds(self) -> Bounds: """Return the X-Y bounding box.""" geom_bounds = list( zip(*(geom.bounds for geom in self.geoms)), ) return ( min(geom_bounds[0]), min(geom_bounds[1]), max(geom_bounds[2]), max(geom_bounds[3]), ) class MultiPoint(_MultiGeometry): """ A collection of one or more points. Attributes ---------- geoms : sequence A sequence of Points """ _geoms: Tuple[Point, ...] def __init__(self, points: Sequence[PointType], unique: bool = False) -> None: """ Create a collection of one or more points. Parameters ---------- points : sequence A sequence of (x, y [,z]) numeric coordinate pairs or triples. unique: boolean, when unique is true duplicates will be removed, the ordering will not be preserved. Example ------- Construct a 2 point collection >>> ob = MultiPoint([[0.0, 0.0], [1.0, 2.0]]) >>> len(ob.geoms) 2 >>> type(ob.geoms[0]) == Point True """ if unique: points = set(points) # type: ignore [assignment] object.__setattr__(self, "_geoms", tuple(Point(*point) for point in points)) def __len__(self) -> int: """Return the number of points in this MultiPoint.""" return len(self._geoms) def __repr__(self) -> str: """Return the representation.""" return f"{self.geom_type}({tuple(geom.coords[0] for geom in self._geoms)})" @property def geoms(self) -> Iterator[Point]: """Iterate over the points.""" yield from (cast(Point, p) for p in super().geoms) @property def _wkt_coords(self) -> str: return ", ".join(point._wkt_coords for point in self.geoms) # noqa: SLF001 @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ geo_interface["coordinates"] = tuple(geom.coords[0] for geom in self.geoms) return geo_interface @classmethod def from_points(cls, *args: Point, unique: bool = False) -> "MultiPoint": """Create a MultiPoint from Points.""" return cls([point.coords[0] for point in args], unique=unique) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "MultiPoint": cls._check_dict(geo_interface) return cls(cast(Sequence[PointType], geo_interface["coordinates"])) def _prepare_hull(self) -> Iterable[Point2D]: return ((pt.x, pt.y) for pt in self.geoms) class MultiLineString(_MultiGeometry): """ A collection of one or more line strings. A MultiLineString has non-zero length and zero area. Attributes ---------- geoms : sequence A sequence of LineStrings """ _geoms: Tuple[LineString, ...] def __init__(self, lines: Sequence[LineType], unique: bool = False) -> None: """ Initialize the MultiLineString. Parameters ---------- lines : sequence A sequence of line-like coordinate sequences. unique: boolean, when unique is true duplicates will be removed, the ordering will not be preserved. Example ------- Construct a collection containing one line string. >>> lines = MultiLineString( [[[0.0, 0.0], [1.0, 2.0]]] ) """ if unique: lines = {tuple(line) for line in lines} # type: ignore [assignment] object.__setattr__(self, "_geoms", tuple(LineString(line) for line in lines)) def __len__(self) -> int: """Return the number of lines in the collection.""" return len(self._geoms) def __repr__(self) -> str: """Return the representation.""" return f"{self.geom_type}({tuple(geom.coords for geom in self._geoms)})" @property def geoms(self) -> Iterator[LineString]: """Iterate over the points.""" yield from (cast(LineString, line) for line in super().geoms) @property def _wkt_coords(self) -> str: return ",".join( f"({linestring._wkt_coords})" for linestring in self.geoms # noqa: SLF001 ) @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ geo_interface["coordinates"] = tuple(geom.coords for geom in self.geoms) return geo_interface @classmethod def from_linestrings( cls, *args: LineString, unique: bool = False, ) -> "MultiLineString": """Create a MultiLineString from LineStrings.""" return cls([line.coords for line in args], unique=unique) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "MultiLineString": cls._check_dict(geo_interface) return cls(cast(Sequence[LineType], geo_interface["coordinates"])) def _prepare_hull(self) -> Iterable[Point2D]: return ( (pt.x, pt.y) for pt in chain.from_iterable(line.geoms for line in self.geoms) ) class MultiPolygon(_MultiGeometry): """ A collection of one or more polygons. If component polygons overlap the collection is `invalid` and some operations on it may fail. Attributes ---------- geoms : sequence A sequence of `Polygon` instances """ _geoms: Tuple[Polygon, ...] def __init__(self, polygons: Sequence[PolygonType], unique: bool = False) -> None: """ Initialize a Multipolygon. Parameters ---------- polygons : sequence A sequence of (shell, holes) tuples where shell is the sequence representation of a linear ring and holes is a sequence of such linear rings unique: boolean, when unique is true duplicates will be removed, the ordering will not be preserved. Example ------- Construct a collection from a sequence of coordinate tuples >>> ob = MultiPolygon([ ... ( ... ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), ... [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))] ...) ...]) >>> len(ob.geoms) 1 >>> type(ob.geoms[0]) == Polygon True """ if unique: polygons = set(polygons) # type: ignore [assignment] object.__setattr__( self, "_geoms", tuple( Polygon( shell=polygon[0], holes=polygon[1] if len(polygon) == 2 else None, # noqa: PLR2004 ) for polygon in polygons ), ) def __len__(self) -> int: """Return the number of polygons in the collection.""" return len(self._geoms) def __repr__(self) -> str: """Return the representation.""" return f"{self.geom_type}({tuple(geom.coords for geom in self.geoms)})" @property def geoms(self) -> Iterator[Polygon]: """Iterate over the points.""" yield from (cast(Polygon, p) for p in super().geoms) @property def _wkt_coords(self) -> str: return ",".join(f"({poly._wkt_coords})" for poly in self.geoms) # noqa: SLF001 @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ coords = tuple( (geom.exterior.coords, *tuple(hole.coords for hole in geom.interiors)) for geom in self.geoms ) geo_interface["coordinates"] = coords return geo_interface @classmethod def from_polygons(cls, *args: Polygon, unique: bool = False) -> "MultiPolygon": """Create a MultiPolygon from Polygons.""" return cls([poly.coords for poly in args], unique=unique) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "MultiPolygon": cls._check_dict(geo_interface) coords = tuple( (poly[0], poly[1:]) # type: ignore [index] for poly in geo_interface["coordinates"] ) return cls(cast(Sequence[PolygonType], coords)) def _prepare_hull(self) -> Iterable[Point2D]: return ( (pt.x, pt.y) for pt in chain.from_iterable(poly.exterior.geoms for poly in self.geoms) ) Geometry = Union[ Point, LineString, LinearRing, Polygon, MultiPoint, MultiLineString, MultiPolygon, ] class GeometryCollection(_MultiGeometry): """ A heterogenous collection of geometries. Attributes: ---------- geoms : sequence A sequence of geometry instances Please note: GEOMETRYCOLLECTION isn't supported by the Shapefile format. And this sub- class isn't generally supported by ordinary GIS sw (viewers and so on). So it's very rarely used in the real GIS professional world. Example: ------- Initialize Geometries and construct a GeometryCollection >>> from pygeoif import geometry >>> p = geometry.Point(1.0, -1.0) >>> p2 = geometry.Point(1.0, -1.0) >>> geoms = [p, p2] >>> c = geometry.GeometryCollection(geoms) >>> c.__geo_interface__ {'type': 'GeometryCollection', 'geometries': [{'type': 'Point', 'coordinates': (1.0, -1.0)}, {'type': 'Point', 'coordinates': (1.0, -1.0)}]} """ _geoms: Tuple[Union[Geometry, "GeometryCollection"], ...] def __init__( self, geometries: Iterable[Union[Geometry, "GeometryCollection"]], ) -> None: """ Initialize the MultiGeometry with Geometries. Args: ---- geometries (Iterable[Geometry] """ object.__setattr__(self, "_geoms", tuple(geom for geom in geometries if geom)) def __eq__(self, other: object) -> bool: """ Return equality between collections. Types and coordinates from all contained geometries must be equal. """ try: if self.is_empty: return False if ( other.__geo_interface__.get("type") # type: ignore [attr-defined] != self.geom_type ): return False if len( other.__geo_interface__.get( # type: ignore [attr-defined] "geometries", [], ), ) != len( self, ): return False except AttributeError: return False return compare_geo_interface( first=self.__geo_interface__, second=other.__geo_interface__, # type: ignore [attr-defined] ) def __len__(self) -> int: """ Length of the collection. Returns ------- int: Number of geometries in the collection. """ return len(self._geoms) def __repr__(self) -> str: """Return the representation.""" return f"{self.geom_type}({tuple(self.geoms)})" @property def _wkt_coords(self) -> str: return ", ".join(geom.wkt for geom in self.geoms) @property def __geo_interface__(self) -> GeoCollectionInterface: # type: ignore [override] """Return the geo interface of the collection.""" return { "type": "GeometryCollection", "geometries": tuple(geom.__geo_interface__ for geom in self.geoms), } def _prepare_hull(self) -> Iterable[Point2D]: return chain.from_iterable( geom._prepare_hull() for geom in self.geoms # noqa: SLF001 ) __all__ = [ "Geometry", "GeometryCollection", "LineString", "LinearRing", "MultiLineString", "MultiPoint", "MultiPolygon", "Point", "Polygon", ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1711389497.4274578 pygeoif-1.4.0/pygeoif/hypothesis/0000755000175100001770000000000014600335471016427 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/hypothesis/__init__.py0000644000175100001770000000005114600335465020537 0ustar00runnerdocker"""Hypothesis strategies for pygeoif.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/hypothesis/strategies.py0000644000175100001770000004256214600335465021167 0ustar00runnerdocker""" Data-generating strategies for property-based testing. Coordinates are limited to 32 bit floats to avoid precision issues. """ from dataclasses import dataclass from functools import partial from typing import Optional from typing import Tuple from typing import cast import hypothesis.strategies as st from pygeoif.geometry import GeometryCollection from pygeoif.geometry import LinearRing from pygeoif.geometry import LineString from pygeoif.geometry import MultiLineString from pygeoif.geometry import MultiPoint from pygeoif.geometry import MultiPolygon from pygeoif.geometry import Point from pygeoif.geometry import Polygon from pygeoif.types import LineType from pygeoif.types import Point2D from pygeoif.types import Point3D from pygeoif.types import PointType __all__ = [ "Srs", "geometry_collections", "line_coords", "line_strings", "linear_rings", "multi_line_strings", "multi_points", "multi_polygons", "point_coords", "points", "polygons", ] coordinate = partial( st.floats, allow_infinity=False, allow_nan=False, allow_subnormal=False, width=32, ) @dataclass(frozen=True) class Srs: """ Represents a spatial reference system (SRS). Attributes ---------- name (str): The name of the SRS. min_xyz (Tuple[Optional[float], Optional[float], Optional[float]]): - The minimum x, y, and z values of the SRS. max_xyz (Tuple[Optional[float], Optional[float], Optional[float]]): - The maximum x, y, and z values of the SRS. """ name: str min_xyz: Tuple[Optional[float], Optional[float], Optional[float]] max_xyz: Tuple[Optional[float], Optional[float], Optional[float]] def longitudes(self) -> st.SearchStrategy[float]: """ Generate a search strategy for generating longitudes. Returns a search strategy that generates floats within the longitude bounds of the SRS. """ return coordinate(min_value=self.min_xyz[0], max_value=self.max_xyz[0]) def latitudes(self) -> st.SearchStrategy[float]: """ Generate a search strategy for generating latitudes. Returns a search strategy that generates floats within the latitude bounds of the SRS. """ return coordinate(min_value=self.min_xyz[1], max_value=self.max_xyz[1]) def elevations(self) -> st.SearchStrategy[float]: """ Generate a search strategy for generating elevations. Returns a search strategy that generates floats within the elevation bounds of the SRS. """ return coordinate(min_value=self.min_xyz[2], max_value=self.max_xyz[2]) epsg4326 = Srs( name="EPSG:4326", min_xyz=(-180.0, -90.0, -999_999_995_904.0), max_xyz=(180.0, 90.0, 999_999_995_904.0), ) @st.composite def _point_coords_2d( draw: st.DrawFn, *, srs: Optional[Srs] = None, ) -> Point2D: """ Generate 2D points using the given draw function. Args: ---- draw: The draw function used to generate the points. srs: Optional spatial reference system (Srs) object. Returns: ------- A tuple of latitude and longitude values generated using the draw function. """ longitudes = coordinate() latitudes = coordinate() if srs: longitudes = srs.longitudes() latitudes = srs.latitudes() return draw(st.tuples(latitudes, longitudes)) @st.composite def _point_coords_3d( draw: st.DrawFn, *, srs: Optional[Srs] = None, ) -> Point3D: """ Generate 3D points using the given draw function. Args: ---- draw: The draw function used to generate the points. srs: Optional spatial reference system (Srs) object. Returns: ------- A tuple representing the generated 3D points. """ longitudes = coordinate() latitudes = coordinate() elevations = coordinate() if srs: longitudes = srs.longitudes() latitudes = srs.latitudes() elevations = srs.elevations() return draw(st.tuples(latitudes, longitudes, elevations)) @st.composite def point_coords( draw: st.DrawFn, *, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> PointType: """ Generate a random point in either 2D or 3D space. Args: ---- draw: The draw function from the hypothesis library. srs: An optional parameter specifying the spatial reference system. has_z: An optional parameter specifying whether to generate 2D or 3D points. Returns: ------- A tuple representing the point in either 2D or 3D space. """ if has_z is True: return draw(_point_coords_3d(srs=srs)) if has_z is False: return draw(_point_coords_2d(srs=srs)) return draw(st.one_of(_point_coords_2d(srs=srs), _point_coords_3d(srs=srs))) @st.composite def points( draw: st.DrawFn, *, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> Point: """ Generate a random point in either 2D or 3D space. Args: ---- draw: The draw function from the hypothesis library. srs: An optional parameter specifying the spatial reference system. has_z: An optional parameter specifying whether to generate 2D or 3D points. Returns: ------- A randomly generated point in either 2D or 3D space. """ return Point(*draw(point_coords(srs=srs, has_z=has_z))) @st.composite def line_coords( # noqa: PLR0913 draw: st.DrawFn, *, min_points: int, max_points: Optional[int] = None, srs: Optional[Srs] = None, has_z: Optional[bool] = None, unique: bool = False, ) -> LineType: """ Generate a random line in either 2D or 3D space. Args: ---- draw: The draw function from the hypothesis library. min_points: Minimum number of points in the line max_points: Maximum number of points in the line srs: An optional parameter specifying the spatial reference system. has_z: An optional parameter specifying whether to generate 2D or 3D points. unique: Optional flag to generate unique points (default False). Returns: ------- A list of point coordinates representing the line in either 2D or 3D space. """ if has_z is None: has_z = draw(st.booleans()) return cast( LineType, draw( st.lists( point_coords(srs=srs, has_z=has_z), min_size=min_points, max_size=max_points, unique=unique, ), ), ) @st.composite def line_strings( draw: st.DrawFn, *, max_points: Optional[int] = None, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> LineString: """ Generate a random linestring in either 2D or 3D space. Args: ---- draw: The draw function from the hypothesis library. max_points: Maximum number of points in the line (must be greater than 1) srs: An optional parameter specifying the spatial reference system. has_z: An optional parameter specifying whether to generate 2D or 3D lines. Returns: ------- A LineString representing the randomly generated linestring in either 2D or 3D space. """ if max_points is not None and max_points < 2: # noqa: PLR2004 raise ValueError("max_points must be greater than 1") # noqa: TRY003,EM101 return LineString( draw( line_coords( min_points=2, max_points=max_points, srs=srs, has_z=has_z, unique=True, ), ), ) @st.composite def linear_rings( draw: st.DrawFn, *, max_points: Optional[int] = None, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> LinearRing: """ Generate a linear ring using the provided draw function. Args: ---- draw (st.DrawFn): The draw function used to generate the coordinates. max_points (Optional[int]): The maximum number of points in the linear ring. If not specified, there is no limit. srs (Optional[Srs]): The spatial reference system of the linear ring. If not specified, the default SRS is used. has_z (Optional[bool]): Whether the linear ring has z-coordinates. If not specified, 2D or 3D coordinates are generated. Returns: ------- LinearRing: The generated linear ring. Raises: ------ ValueError: If max_points is less than 4. """ if max_points is not None and max_points < 4: # noqa: PLR2004 raise ValueError("max_points must be greater than 3") # noqa: TRY003,EM101 return LinearRing( draw( line_coords( min_points=3, max_points=max_points, srs=srs, has_z=has_z, unique=True, ), ), ) @st.composite def polygons( # noqa: PLR0913 draw: st.DrawFn, *, max_points: Optional[int] = None, min_interiors: int = 0, max_interiors: int = 5, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> Polygon: """ Generate a random polygon using the given strategies. Args: ---- draw (st.DrawFn): The draw function used to generate random values. max_points (Optional[int]): The maximum number of points in the polygon. If not specified, there is no limit. min_interiors (Optional[int]): The minimum number of interior rings (holes) in the polygon. Defaults to 0. max_interiors (Optional[int]): The maximum number of interior rings (holes) in the polygon. If not specified, there is no limit. srs (Optional[Srs]): The spatial reference system of the polygon. Defaults to None. has_z (Optional[bool]): Whether the polygon has z-coordinates. If not specified, a random boolean value will be used. Returns: ------- Polygon: The generated polygon. Raises: ------ ValueError: If max_points is specified and is less than 4. """ if has_z is None: has_z = draw(st.booleans()) if max_points is not None and max_points < 4: # noqa: PLR2004 raise ValueError("max_points must be greater than 3") # noqa: TRY003,EM101 return Polygon( draw( line_coords( min_points=3, max_points=max_points, srs=srs, has_z=has_z, unique=True, ), ), holes=draw( st.lists( line_coords( min_points=3, max_points=max_points, srs=srs, has_z=has_z, unique=True, ), min_size=min_interiors, max_size=max_interiors, ), ), ) @st.composite def multi_points( draw: st.DrawFn, *, min_points: int = 1, max_points: Optional[int] = None, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> MultiPoint: """ Generate a MultiPoint geometry object with random coordinates. Args: ---- draw (st.DrawFn): The draw function from the hypothesis library. min_points (int): The minimum number of points in the MultiPoint. Default is 1. max_points (Optional[int]): The maximum number of points in the MultiPoint. srs (Optional[Srs]): The spatial reference system of the coordinates. has_z (Optional[bool]): Whether the coordinates have a Z component. if not specified, 2D and 3D coordinates will be generated randomly. Returns: ------- MultiPoint: The generated MultiPoint geometry object. """ if has_z is None: has_z = draw(st.booleans()) return MultiPoint( draw( st.lists( point_coords(srs=srs, has_z=has_z), min_size=min_points, max_size=max_points, unique=True, ), ), ) @st.composite def multi_line_strings( # noqa: PLR0913 draw: st.DrawFn, *, min_lines: int = 1, max_lines: int = 5, max_points: int = 10, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> MultiLineString: """ Generate a random MultiLineString object. Args: ---- draw (st.DrawFn): The Hypothesis draw function. min_lines (int, optional): The minimum number of lines in the MultiLineString. max_lines (int, optional): The maximum number of lines in the MultiLineString. max_points (int, optional): The maximum number of points in each line. srs (Srs, optional): The spatial reference system of the MultiLineString. has_z (bool, optional): Whether the MultiLineString has z-coordinates. Returns: ------- MultiLineString: The generated MultiLineString object. """ if has_z is None: has_z = draw(st.booleans()) return MultiLineString( draw( st.lists( line_coords( min_points=2, max_points=max_points, srs=srs, has_z=has_z, unique=True, ), min_size=min_lines, max_size=max_lines, ), ), ) @st.composite def multi_polygons( # noqa: PLR0913 draw: st.DrawFn, *, min_polygons: int = 1, max_polygons: int = 3, max_points: int = 10, min_interiors: int = 0, max_interiors: int = 2, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> MultiPolygon: """ Generate a random MultiPolygon object. Args: ---- draw (st.DrawFn): The Hypothesis draw function. min_polygons (int, optional): The min number of polygons in the MultiPolygon. max_polygons (int, optional): The max number of polygons in the MultiPolygon. max_points (int, optional): The maximum number of points in each polygon. min_interiors (int, optional): The minimum number of interiors in each polygon. max_interiors (int, optional): The maximum number of interiors in each polygon. srs (Optional[Srs], optional): The spatial reference system of the MultiPolygon. has_z (Optional[bool], optional): Whether the MultiPolygon has z-coordinates. Returns: ------- MultiPolygon: The generated MultiPolygon object. """ if has_z is None: has_z = draw(st.booleans()) return MultiPolygon.from_polygons( *draw( st.lists( polygons( max_points=max_points, min_interiors=min_interiors, max_interiors=max_interiors, srs=srs, has_z=has_z, ), min_size=min_polygons, max_size=max_polygons, ), ), ) @st.composite def geometry_collections( # noqa: PLR0913 draw: st.DrawFn, *, min_geoms: int = 1, max_geoms: int = 5, max_points: int = 20, min_interiors: int = 0, max_interiors: int = 5, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> GeometryCollection: """ Generate a random GeometryCollection object. Args: ---- draw (st.DrawFn): The Hypothesis draw function. min_geoms (int, optional): The minimum number of geometries in the collection. max_geoms (int, optional): The maximum number of geometries in the collection. max_points (int, optional): The maximum number of points in each geometry. min_interiors (int, optional): The minimum number of interiors in each polygon. max_interiors (int, optional): The maximum number of interiors in each polygon. srs (Optional[Srs], optional): The spatial reference system of the geometries. has_z (Optional[bool], optional): Whether the geometries have Z coordinates. Returns: ------- GeometryCollection: A randomly generated GeometryCollection object. """ if has_z is None: has_z = draw(st.booleans()) return GeometryCollection( draw( st.lists( st.one_of( points(srs=srs, has_z=has_z), line_strings(max_points=max_points, srs=srs, has_z=has_z), linear_rings(max_points=max_points, srs=srs, has_z=has_z), polygons( max_points=max_points, min_interiors=min_interiors, max_interiors=max_interiors, srs=srs, has_z=has_z, ), multi_points(max_points=max_points, srs=srs, has_z=has_z), multi_line_strings( max_points=max_points, max_lines=max_geoms, srs=srs, has_z=has_z, ), multi_polygons( max_points=max_points, min_interiors=min_interiors, max_interiors=max_interiors, max_polygons=max_geoms, srs=srs, has_z=has_z, ), ), min_size=min_geoms, max_size=max_geoms, ), ), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/py.typed0000644000175100001770000000000014600335465015720 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pygeoif/types.py0000644000175100001770000000700014600335465015746 0ustar00runnerdocker# # Copyright (C) 2012 - 2024 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 Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public License # along with this library; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Types for geometries.""" from typing import Any from typing import Dict from typing import Optional from typing import Sequence from typing import Tuple from typing import Union from typing_extensions import Literal from typing_extensions import NotRequired from typing_extensions import Protocol from typing_extensions import TypedDict # for Python <3.11 with (Not)Required Point2D = Tuple[float, float] Point3D = Tuple[float, float, float] PointType = Union[Point2D, Point3D] Line2D = Sequence[Point2D] Line3D = Sequence[Point3D] LineType = Union[Line2D, Line3D] Interiors = Optional[Sequence[LineType]] Poly2d = Union[ Tuple[Line2D, Sequence[Line2D]], Tuple[Line2D], ] Poly3d = Union[ Tuple[Line3D, Sequence[Line3D]], Tuple[Line3D], ] PolygonType = Union[ Poly2d, Poly3d, ] MultiGeometryType = Sequence[Union[PointType, LineType, PolygonType]] Bounds = Tuple[float, float, float, float] CoordinatesType = Union[ PointType, LineType, Sequence[LineType], ] MultiCoordinatesType = Sequence[CoordinatesType] class GeoInterface(TypedDict): """Required keys for the GeoInterface.""" type: str coordinates: Union[CoordinatesType, MultiCoordinatesType] bbox: NotRequired[Bounds] class GeoCollectionInterface(TypedDict): """Geometry Collection Interface.""" type: Literal["GeometryCollection"] geometries: Sequence[Union[GeoInterface, "GeoCollectionInterface"]] bbox: NotRequired[Bounds] class GeoFeatureInterface(TypedDict): """The GeoFeatureInterface has optional keys.""" type: Literal["Feature"] bbox: NotRequired[Bounds] properties: NotRequired[Dict[str, Any]] id: NotRequired[Union[str, int]] geometry: GeoInterface class GeoFeatureCollectionInterface(TypedDict): """Bbox and id are optional keys for the GeoFeatureCollectionInterface.""" type: Literal["FeatureCollection"] features: Sequence[GeoFeatureInterface] bbox: NotRequired[Bounds] id: NotRequired[Union[str, int]] class GeoType(Protocol): """Any compatible type that implements the __geo_interface__.""" @property def __geo_interface__(self) -> GeoInterface: """Return the GeoInterface.""" class GeoCollectionType(Protocol): """Any compatible type that implements the __geo_interface__.""" @property def __geo_interface__(self) -> GeoCollectionInterface: """Return the GeoInterface.""" __all__ = [ "Bounds", "CoordinatesType", "Interiors", "GeoCollectionInterface", "GeoCollectionType", "GeoFeatureCollectionInterface", "GeoFeatureInterface", "GeoInterface", "GeoType", "LineType", "MultiCoordinatesType", "MultiGeometryType", "Point2D", "Point3D", "PointType", "PolygonType", ] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1711389497.431458 pygeoif-1.4.0/pygeoif.egg-info/0000755000175100001770000000000014600335471015722 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389497.0 pygeoif-1.4.0/pygeoif.egg-info/PKG-INFO0000644000175100001770000003430014600335471017017 0ustar00runnerdockerMetadata-Version: 2.1 Name: pygeoif Version: 1.4.0 Summary: A basic implementation of the __geo_interface__ Author-email: Christian Ledermann License: LGPL Project-URL: Changelog, https://github.com/cleder/pygeoif/blob/develop/docs/HISTORY.rst Project-URL: Documentation, https://pygeoif.readthedocs.io/ Project-URL: Homepage, https://github.com/cleder/pygeoif/ Keywords: GIS,Spatial,WKT Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Scientific/Engineering :: GIS Classifier: Typing :: Typed Requires-Python: >=3.8 Description-Content-Type: text/x-rst Requires-Dist: typing_extensions Provides-Extra: complexity Requires-Dist: lizard; extra == "complexity" Requires-Dist: radon; extra == "complexity" Provides-Extra: dev Requires-Dist: pre-commit; extra == "dev" Requires-Dist: pygeoif[complexity]; extra == "dev" Requires-Dist: pygeoif[linting]; extra == "dev" Requires-Dist: pygeoif[tests]; extra == "dev" Requires-Dist: pygeoif[typing]; extra == "dev" Provides-Extra: hypothesis Requires-Dist: hypothesis; extra == "hypothesis" Provides-Extra: linting Requires-Dist: black; extra == "linting" Requires-Dist: flake8; extra == "linting" Requires-Dist: flake8-cognitive-complexity; extra == "linting" Requires-Dist: flake8-comments; extra == "linting" Requires-Dist: flake8-complex-f-strings; extra == "linting" Requires-Dist: flake8-continuation; extra == "linting" Requires-Dist: flake8-docstrings; extra == "linting" Requires-Dist: flake8-dunder-all; extra == "linting" Requires-Dist: flake8-encodings; extra == "linting" Requires-Dist: flake8-expression-complexity; extra == "linting" Requires-Dist: flake8-function-order; extra == "linting" Requires-Dist: flake8-length; extra == "linting" Requires-Dist: flake8-pep3101; extra == "linting" Requires-Dist: flake8-rst-docstrings; extra == "linting" Requires-Dist: flake8-string-format; extra == "linting" Requires-Dist: flake8-super; extra == "linting" Requires-Dist: flake8-typing-imports; extra == "linting" Requires-Dist: flake8-use-fstring; extra == "linting" Requires-Dist: ruff; extra == "linting" Requires-Dist: yamllint; extra == "linting" Provides-Extra: tests Requires-Dist: hypothesis; extra == "tests" Requires-Dist: pytest; extra == "tests" Requires-Dist: pytest-cov; extra == "tests" Provides-Extra: typing Requires-Dist: hypothesis; extra == "typing" Requires-Dist: mypy; extra == "typing" Introduction ============ .. inclusion-marker-do-not-remove PyGeoIf provides a `GeoJSON-like protocol `_ for geo-spatial (GIS) vector data. Other Python programs and packages that you may have heard of that implement this protocol: * `ArcPy `_ * `descartes `_ * `PySAL `_ * `Shapely `_ * `pyshp `_ * `GeoPandas `_ * `Karta `_ * `mapnik `_ When you want to write your own geospatial library with support for this protocol you may use pygeoif as a starting point and build your functionality on top of it. It has no requirements outside the Python standard library and is therefore easy to integrate into your project. It is tested on `CPython `_ and `PyPy `_, but it should work on alternative Python implementations (that implement the language specification *>=3.8*) as well. You may think of pygeoif as a 'shapely ultralight' which lets you construct geometries and perform **very** basic operations like reading and writing geometries from/to WKT, constructing line strings out of points, polygons from linear rings, multi polygons from polygons, etc. It was inspired by shapely and implements the geometries in a way that when you are familiar with pygeoif, you will feel right at home with shapely or the other way round. It provides Hypothesis strategies for all geometries for property based testing with Hypothesis_. It was written to provide clean and python only geometries for fastkml_ .. image:: https://readthedocs.org/projects/pygeoif/badge/?version=latest :target: https://pygeoif.readthedocs.io/en/latest/?badge=latest :alt: Documentation .. image:: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml/badge.svg?branch=main :target: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml :alt: GitHub Actions .. image:: https://codecov.io/gh/cleder/pygeoif/branch/main/graph/badge.svg?token=2EfiwBXs9X :target: https://codecov.io/gh/cleder/pygeoif :alt: Codecov .. image:: https://img.shields.io/badge/property_based_tests-hypothesis-green :target: https://hypothesis.works :alt: Hypothesis .. image:: https://img.shields.io/badge/code_style-black-000000.svg :target: https://github.com/psf/ :alt: Black .. image:: https://img.shields.io/badge/type_checker-mypy-blue :target: http://mypy-lang.org/ :alt: Mypy .. image:: https://www.openhub.net/p/pygeoif/widgets/project_thin_badge.gif :target: https://www.openhub.net/p/pygeoif/ :alt: Openhub .. image:: https://www.codefactor.io/repository/github/cleder/pygeoif/badge/main :target: https://www.codefactor.io/repository/github/cleder/pygeoif/overview/main :alt: CodeFactor .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit :target: https://github.com/pre-commit/pre-commit :alt: pre-commit .. image:: https://img.shields.io/pypi/pyversions/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/implementation/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python implementations .. image:: https://img.shields.io/pypi/v/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: License .. image:: https://img.shields.io/pypi/dm/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Downloads Installation ------------ You can install PyGeoIf from pypi using pip:: pip install pygeoif Example ======== >>> from pygeoif import geometry >>> p = geometry.Point(1,1) >>> p.__geo_interface__ {'type': 'Point', 'bbox': (1, 1, 1, 1), 'coordinates': (1, 1)} >>> print(p) POINT (1 1) >>> p Point(1, 1) >>> l = geometry.LineString([(0.0, 0.0), (1.0, 1.0)]) >>> l.bounds (0.0, 0.0, 1.0, 1.0) >>> print(l) LINESTRING (0.0 0.0, 1.0 1.0) You find more examples in the `tests `_ directory which cover every aspect of pygeoif or in fastkml_. Classes ======== All classes implement the attribute: * ``__geo_interface__``: as discussed above, an interface to GeoJSON_. All geometry classes implement the attributes: * ``geom_type``: Returns a string specifying the Geometry Type of the object * ``bounds``: Returns a (minx, miny, maxx, maxy) tuple that bounds the object. * ``wkt``: Returns the 'Well Known Text' representation of the object For two-dimensional geometries the following methods are implemented: * ``convex_hull``: Returns a representation of the smallest convex Polygon containing all the points in the object unless the number of points in the object is less than three. For two points, the convex hull collapses to a LineString; for 1, a Point. For three dimensional objects only their projection in the xy plane is taken into consideration. Empty objects without coordinates return ``None`` for the convex_hull. Point ----- A zero dimensional geometry A point has zero length and zero area. A point cannot be empty. Attributes ~~~~~~~~~~~ x, y, z : float Coordinate values Example ~~~~~~~~ >>> from pygeoif import Point >>> p = Point(1.0, -1.0) >>> print(p) POINT (1.0 -1.0) >>> p.y -1.0 >>> p.x 1.0 LineString ----------- A one-dimensional figure comprising one or more line segments A LineString has non-zero length and zero area. It may approximate a curve and need not be straight. Unlike a LinearRing, a LineString is not closed. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of Points LinearRing ----------- A closed one-dimensional geometry comprising one or more line segments A LinearRing that crosses itself or touches itself at a single point is invalid and operations on it may fail. A LinearRing is self closing. Polygon -------- A two-dimensional figure bounded by a linear ring A polygon has a non-zero area. It may have one or more negative-space "holes" which are also bounded by linear rings. If any rings cross each other, the geometry is invalid and operations on it may fail. Attributes ~~~~~~~~~~~ exterior : LinearRing The ring which bounds the positive space of the polygon. interiors : sequence A sequence of rings which bound all existing holes. maybe_valid: boolean When a polygon has obvious problems such as self crossing lines or holes that are outside the exterior bounds this will return False. Even if this returns True the geometry may still be invalid, but if this returns False you do have a problem. MultiPoint ---------- A collection of one or more points. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of Points. MultiLineString ---------------- A collection of one or more line strings. A MultiLineString has non-zero length and zero area. Attributes ~~~~~~~~~~~ geoms : sequence A sequence of LineStrings MultiPolygon ------------- A collection of one or more polygons. Attributes ~~~~~~~~~~~~~ geoms : sequence A sequence of `Polygon` instances GeometryCollection ------------------- A heterogenous collection of geometries (Points, LineStrings, LinearRings and Polygons). Attributes ~~~~~~~~~~~ geoms : sequence A sequence of geometry instances Please note: ``GEOMETRYCOLLECTION`` isn't supported by the Shapefile or GeoJSON_ format. And this sub-class isn't generally supported by ordinary GIS sw (viewers and so on). So it's very rarely used in the real GIS professional world. Example ~~~~~~~~ >>> from pygeoif import geometry >>> p = geometry.Point(1.0, -1.0) >>> p2 = geometry.Point(1.0, -1.0) >>> geoms = [p, p2] >>> c = geometry.GeometryCollection(geoms) >>> [geom for geom in geoms] [Point(1.0, -1.0), Point(1.0, -1.0)] Feature ------- Aggregates a geometry instance with associated user-defined properties. Attributes ~~~~~~~~~~~ geometry : object A geometry instance properties : dict A dictionary linking field keys with values associated with with geometry instance Example ~~~~~~~~ >>> from pygeoif import Point, Feature >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = Feature(p, props) >>> a.properties {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a.properties['Name'] 'Sample Point' FeatureCollection ----------------- A heterogenous collection of Features Attributes ~~~~~~~~~~~ features: sequence A sequence of feature instances Example ~~~~~~~~ >>> from pygeoif import Point, Feature, FeatureCollection >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} >>> a = Feature(p, props) >>> p2 = Point(1.0, -1.0) >>> props2 = {'Name': 'Sample Point2', 'Other': 'Other Data2'} >>> b = Feature(p2, props2) >>> features = [a, b] >>> c = FeatureCollection(features) >>> [feature for feature in c] [Feature(Point(1.0, -1.0), {'Name': 'Sample Point', 'Other': 'Other Data'},...] Functions ========= shape -------- Create a pygeoif feature from an object that provides the ``__geo_interface__`` or any GeoJSON_ compatible dictionary. >>> from shapely.geometry import Point >>> from pygeoif import geometry, shape >>> shape(Point(0,0)) Point(0.0, 0.0) from_wkt --------- Create a geometry from its WKT representation >>> from pygeoif import from_wkt >>> p = from_wkt('POINT (0 1)') >>> print(p) POINT (0.0 1.0) signed_area ------------ Return the signed area enclosed by a ring. A value >= 0 indicates a counter-clockwise oriented ring. orient ------- Returns a copy of a polygon with exteriors and interiors in the right orientation. if ccw is True than the exterior will be in counterclockwise orientation and the interiors will be in clockwise orientation, or the other way round when ccw is False. box --- Return a rectangular polygon with configurable normal vector. mapping ------- Return the ``__geo_interface__`` dictionary. Development =========== Clone this repository, create a virtualenv with Python 3.8 or later with ``python3 -m venv .venv`` and activate it with ``source .venv/bin/activate``. Then install the requirements with ``pip install -e ".[dev]"``. 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 Testing ------- Run the unit and static tests with:: pytest tests pytest --doctest-glob="README.rst" black pygeoif ruff pygeoif flake8 pygeoif mypy pygeoif Acknowledgments ================ The tests were improved with mutmut_ which discovered some nasty edge cases. .. _mutmut: https://github.com/boxed/mutmut .. _GeoJSON: https://geojson.org/ .. _fastkml: http://pypi.python.org/pypi/fastkml/ .. _Hypothesis: https://hypothesis.works ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389497.0 pygeoif-1.4.0/pygeoif.egg-info/SOURCES.txt0000644000175100001770000000132314600335471017605 0ustar00runnerdockerREADME.rst pyproject.toml pygeoif/__init__.py pygeoif/about.py pygeoif/exceptions.py pygeoif/factories.py pygeoif/feature.py pygeoif/functions.py pygeoif/geometry.py pygeoif/py.typed pygeoif/types.py pygeoif.egg-info/PKG-INFO pygeoif.egg-info/SOURCES.txt pygeoif.egg-info/dependency_links.txt pygeoif.egg-info/requires.txt pygeoif.egg-info/top_level.txt pygeoif/hypothesis/__init__.py pygeoif/hypothesis/strategies.py tests/test_base.py tests/test_bounds.py tests/test_factories.py tests/test_feature.py tests/test_functions.py tests/test_geometrycollection.py tests/test_line.py tests/test_linear_ring.py tests/test_multiline.py tests/test_multipoint.py tests/test_multipolygon.py tests/test_point.py tests/test_polygon.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389497.0 pygeoif-1.4.0/pygeoif.egg-info/dependency_links.txt0000644000175100001770000000000114600335471021770 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389497.0 pygeoif-1.4.0/pygeoif.egg-info/requires.txt0000644000175100001770000000110114600335471020313 0ustar00runnerdockertyping_extensions [complexity] lizard radon [dev] pre-commit pygeoif[complexity] pygeoif[linting] pygeoif[tests] pygeoif[typing] [hypothesis] hypothesis [linting] black flake8 flake8-cognitive-complexity flake8-comments flake8-complex-f-strings flake8-continuation flake8-docstrings flake8-dunder-all flake8-encodings flake8-expression-complexity flake8-function-order flake8-length flake8-pep3101 flake8-rst-docstrings flake8-string-format flake8-super flake8-typing-imports flake8-use-fstring ruff yamllint [tests] hypothesis pytest pytest-cov [typing] hypothesis mypy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389497.0 pygeoif-1.4.0/pygeoif.egg-info/top_level.txt0000644000175100001770000000001014600335471020443 0ustar00runnerdockerpygeoif ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/pyproject.toml0000644000175100001770000001123514600335465015507 0ustar00runnerdocker[build-system] build-backend = "setuptools.build_meta" requires = [ "setuptools>=61.2", ] [project] authors = [ { email = "christian.ledermann@gmail.com", name = "Christian Ledermann" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Scientific/Engineering :: GIS", "Typing :: Typed", ] dependencies = [ "typing_extensions", ] description = "A basic implementation of the __geo_interface__" dynamic = [ "version", ] keywords = [ "GIS", "Spatial", "WKT", ] name = "pygeoif" requires-python = ">=3.8" [project.license] text = "LGPL" [project.optional-dependencies] complexity = [ "lizard", "radon", ] dev = [ "pre-commit", "pygeoif[complexity]", "pygeoif[linting]", "pygeoif[tests]", "pygeoif[typing]", ] hypothesis = [ "hypothesis", ] linting = [ "black", "flake8", "flake8-cognitive-complexity", "flake8-comments", "flake8-complex-f-strings", "flake8-continuation", "flake8-docstrings", "flake8-dunder-all", "flake8-encodings", "flake8-expression-complexity", "flake8-function-order", "flake8-length", "flake8-pep3101", "flake8-rst-docstrings", "flake8-string-format", "flake8-super", "flake8-typing-imports", "flake8-use-fstring", "ruff", "yamllint", ] tests = [ "hypothesis", "pytest", "pytest-cov", ] typing = [ "hypothesis", "mypy", ] [project.readme] content-type = "text/x-rst" file = "README.rst" [project.urls] Changelog = "https://github.com/cleder/pygeoif/blob/develop/docs/HISTORY.rst" Documentation = "https://pygeoif.readthedocs.io/" Homepage = "https://github.com/cleder/pygeoif/" [tool.coverage.paths] source = [ "pygeoif", "tests", ] [tool.coverage.run] branch = true [tool.flake8] max_line_length = 88 [tool.isort] force_single_line = true line_length = 88 [tool.mypy] disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true enable_error_code = [ "ignore-without-code", ] ignore_errors = false ignore_missing_imports = false implicit_reexport = false no_implicit_optional = true overrides = [ { disallow_untyped_defs = false, ignore_errors = true, module = "tests.*" }, ] show_error_codes = true strict_equality = true strict_optional = true warn_no_return = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true [tool.pyright] exclude = [ "**/__pycache__", "**/node_modules", ".pyre", ".pytype", "pygeoif/tests", ] include = [ "pygeoif", ] reportMissingImports = true reportMissingTypeStubs = true [tool.ruff] fix = true target-version = "py38" [tool.ruff.lint] ignore = [ "ANN101", "ANN102", "D203", "D212", "FA100", ] select = [ "A", "AIR", "ANN", "ARG", "ASYNC", "B", "BLE", "C4", "C90", "COM", "CPY", "D", "DJ", "DTZ", "E", "EM", "ERA", "EXE", "F", "FA", "FBT", "FIX", "FLY", "FURB", "G", "I", "ICN", "INP", "INT", "ISC", "LOG", "N", "NPY", "PD", "PERF", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "SLF", "SLOT", "T10", "T20", "TCH", "TD", "TID", "TRY", "UP", "W", "YTT", ] [tool.ruff.lint.isort] force-single-line = true [tool.ruff.lint.per-file-ignores] "pygeoif/factories.py" = [ "SLF001", ] "pygeoif/geometry.py" = [ "D417", "FBT001", "FBT002", ] "tests/*.py" = [ "ANN001", "ANN201", "D101", "D102", "D103", "FBT001", "FBT003", "PGH001", "PGH003", "PLR2004", "RUF012", "S101", "S307", "S311", "SLF001", ] [tool.setuptools.dynamic.version] attr = "pygeoif.about.__version__" [tool.setuptools.packages.find] exclude = [ "docs*", ] include = [ "pygeoif*", "pygeoif/py.typed", ] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1711389497.435458 pygeoif-1.4.0/setup.cfg0000644000175100001770000000004614600335471014407 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1711389497.431458 pygeoif-1.4.0/tests/0000755000175100001770000000000014600335471013730 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_base.py0000644000175100001770000001066014600335465016261 0ustar00runnerdocker"""Test Baseclass.""" from unittest import mock import pytest from pygeoif import geometry def test_geometry_interface() -> None: """The geo interface must be implemented in subclasses.""" base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo.__geo_interface__ def test_bounds() -> None: """Subclasses must implement bounds.""" base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo.bounds def test_wkt() -> None: """Implement wkt in subclasses.""" base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo.wkt def test_empty() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo.is_empty def test_wkt_inset() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo._wkt_inset == "" def test_wkt_coordinates() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo._wkt_coords def test_from_dict() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo._from_dict({"type": "_Geometry"}) # type: ignore def test_has_z() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo.has_z def test_convex_hull() -> None: with mock.patch("pygeoif.geometry._Geometry.has_z"): base_geo = geometry._Geometry() with pytest.raises( NotImplementedError, match="^Must be implemented by subclass$", ): assert base_geo.convex_hull def test_get_bounds() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo._get_bounds() @pytest.mark.parametrize( ("attr_val", "expected_error", "expected_error_message"), [ # Happy path tests ( ("attribute", "value"), AttributeError, "Attributes of _Geometry cannot be changed", ), ( ("another_attribute", 123), AttributeError, "Attributes of _Geometry cannot be changed", ), ( ("yet_another_attribute", [1, 2, 3]), AttributeError, "Attributes of _Geometry cannot be changed", ), # Edge cases (("", "value"), AttributeError, "Attributes of _Geometry cannot be changed"), ((None, "value"), TypeError, ".*attribute name must be string.*"), # Error cases ((123, "value"), TypeError, ".*attribute name must be string.*"), (([1, 2, 3], "value"), TypeError, ".*attribute name must be string.*"), ], ) def test_setattr(attr_val, expected_error, expected_error_message) -> None: base_geo = geometry._Geometry() with pytest.raises(expected_error, match=f"^{expected_error_message}$"): setattr(base_geo, *attr_val) @pytest.mark.parametrize( ("attr", "expected_error", "expected_error_message"), [ ( "attr1", AttributeError, "Attributes of _Geometry cannot be deleted", ), # realistic test value ( "", AttributeError, "Attributes of _Geometry cannot be deleted", ), # edge case: empty string ( None, TypeError, ".*attribute name must be string.*", ), # edge case: None ( 123, TypeError, ".*attribute name must be string.*", ), # error case: non-string attribute ], ids=[ "realistic_test_value", "edge_case_empty_string", "edge_case_None", "error_case_non_string_attribute", ], ) def test_delattr(attr, expected_error, expected_error_message) -> None: # Arrange base_geo = geometry._Geometry() # Act with pytest.raises(expected_error, match=f"^{expected_error_message}$"): delattr(base_geo, attr) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_bounds.py0000644000175100001770000000254614600335465016645 0ustar00runnerdocker"""Optimize bounds for multigeometries.""" minx = (1, 2, 3, 4) miny = (8, 7, 6, 5) maxx = (5, 6, 7, 8) maxy = (9, 8, 7, 6) def get_bounds(): return zip(minx, miny, maxx, maxy) def test_bounds() -> None: assert list(get_bounds()) == [ (1, 8, 5, 9), (2, 7, 6, 8), (3, 6, 7, 7), (4, 5, 8, 6), ] def test_brute() -> None: new_bounds = ( min(b[0] for b in get_bounds()), min(b[1] for b in get_bounds()), max(b[2] for b in get_bounds()), max(b[3] for b in get_bounds()), ) assert new_bounds == (1, 5, 8, 9) def test_unzip() -> None: assert list(zip(*get_bounds())) == [minx, miny, maxx, maxy] def test_bounds_unzipped() -> None: tb = list(zip(*get_bounds())) new_bounds = ( min(tb[0]), min(tb[1]), max(tb[2]), max(tb[3]), ) assert new_bounds == (1, 5, 8, 9) def get_line(): return zip(minx, miny) def test_line() -> None: assert list(get_line()) == [ (1, 8), (2, 7), (3, 6), (4, 5), ] def test_unzip_line() -> None: assert list(zip(*get_line())) == [(1, 2, 3, 4), (8, 7, 6, 5)] def test_line_bounds() -> None: xy = list(zip(*get_line())) bounds = ( min(xy[0]), min(xy[1]), max(xy[0]), max(xy[1]), ) assert bounds == (1, 5, 4, 8) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_factories.py0000644000175100001770000004613514600335465017334 0ustar00runnerdocker"""Test the geometry factories.""" import pytest from pygeoif import factories from pygeoif import geometry def test_num_int() -> None: assert factories.num("1") == 1 assert isinstance(factories.num("1"), int) def test_num_intf() -> None: assert factories.num("1.0") == 1 assert isinstance(factories.num("1.0"), int) def test_num_float() -> None: assert factories.num("1.1") == 1.1 assert isinstance(factories.num("1.1"), float) def test_force_2d_point() -> None: # 2d point to 2d point (no actual change) p = geometry.Point(-1, 1) p2d = factories.force_2d(p) assert p2d.x == -1 assert p2d.y == 1 assert not p2d.has_z # 3d point to 2d point p = geometry.Point(-1, 1, 2) p2d = factories.force_2d(p) assert p2d.x == -1 assert p2d.y == 1 assert not p2d.has_z def test_force_2d_multipoint() -> None: # 2d to 2d (no actual change) p = geometry.MultiPoint([(-1, 1), (2, 3)]) p2d = factories.force_2d(p) assert list(p2d.geoms) == [geometry.Point(-1, 1), geometry.Point(2, 3)] def test_force_2d_linestring() -> None: # 2d line string to 2d line string (no actual change) ls = geometry.LineString([(1, 2), (3, 4)]) l2d = factories.force_2d(ls) assert l2d.coords == ((1, 2), (3, 4)) # 3d line string to 2d line string ls = geometry.LineString([(1, 2, 3), (4, 5, 6)]) l2d = factories.force_2d(ls) assert l2d.coords == ((1, 2), (4, 5)) def test_force_2d_linearring() -> None: # 2d linear ring to 2d linear ring (no actual change) r = geometry.LinearRing([(1, 2), (3, 4)]) r2d = factories.force_2d(r) assert r2d.coords == ((1, 2), (3, 4), (1, 2)) # 3d linear ring to 2d linear ring r = geometry.LinearRing([(1, 2, 3), (4, 5, 6)]) r2d = factories.force_2d(r) assert r2d.coords == ((1, 2), (4, 5), (1, 2)) def test_force_2d_multilinestring() -> None: # 2d multi line string to 2d multi line string (no actual change) mls = geometry.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]]) mls2d = factories.force_2d(mls) assert list(mls2d.geoms) == list(mls.geoms) # 3d multi line string to 2d multi line string mls = geometry.MultiLineString([[(1, 2, 3), (4, 5, 6)], [(7, 8, 9), (10, 11, 12)]]) mls2d = factories.force_2d(mls) assert list(mls2d.geoms) == [ geometry.LineString([(1, 2), (4, 5)]), geometry.LineString([(7, 8), (10, 11)]), ] def test_force_2d_polygon() -> None: # 2d to 2d (no actual change) external = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)] p = geometry.Polygon(external, [internal]) p2d = factories.force_2d(p) assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0))) assert p2d.coords[1] == ( ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), ) assert not p2d.has_z # 3d to 2d external = [(0, 0, 1), (0, 2, 1), (2, 2, 1), (2, 0, 1), (0, 0, 1)] internal = [ (0.5, 0.5, 1), (0.5, 1.5, 1), (1.5, 1.5, 1), (1.5, 0.5, 1), (0.5, 0.5, 1), ] p = geometry.Polygon(external, [internal]) p2d = factories.force_2d(p) assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0))) assert p2d.coords[1] == ( ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), ) assert not p2d.has_z def test_force_2d_multipolygon() -> None: # 2d to 2d (no actual change) external = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)] mp = geometry.MultiPolygon([(external, [internal]), (external, [internal])]) mp2d = factories.force_2d(mp) assert list(mp2d.geoms) == list(mp.geoms) def test_force2d_collection() -> None: # 2d to 2d (no actual change) gc = geometry.GeometryCollection([geometry.Point(-1, 1), geometry.Point(-2, 2)]) gc2d = factories.force_2d(gc) assert list(gc2d.geoms) == list(gc.geoms) # 3d to 2d gc = geometry.GeometryCollection( [geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)], ) gc2d = factories.force_2d(gc) assert list(gc2d.geoms) == [geometry.Point(-1, 1), geometry.Point(-2, 2)] def test_force_2d_nongeo() -> None: pytest.raises(AttributeError, factories.force_2d, (1, 2, 3)) def test_force_3d_point() -> None: p = geometry.Point(0, 0) p3d = factories.force_3d(p) assert p3d.x == 0 assert p3d.y == 0 assert p3d.z == 0 assert p3d.has_z def test_force_3d_collection() -> None: gc = geometry.GeometryCollection( [geometry.Point(-1, 1), geometry.Point(-2, 2)], ) gc3d = factories.force_3d(gc) assert list(gc3d.geoms) == [geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)] def test_force_3d_point_with_z() -> None: p = geometry.Point(0, 0, 1) p3d = factories.force_3d(p) assert p3d.x == 0 assert p3d.y == 0 assert p3d.z == 1 assert p3d.has_z def test_force_3d_point_noop() -> None: p = geometry.Point(1, 2, 3) p3d = factories.force_3d(p) assert p3d.x == 1 assert p3d.y == 2 assert p3d.z == 3 assert p3d.has_z def test_force_3d_nongeo() -> None: pytest.raises(AttributeError, factories.force_3d, (1, 2)) def test_orient_true() -> None: ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int_1 = [(0.5, 0.25), (1.5, 0.25), (1.5, 1.25), (0.5, 1.25), (0.5, 0.25)] int_2 = [(0.5, 1.25), (1, 1.25), (1, 1.75), (0.5, 1.75), (0.5, 1.25)] p = geometry.Polygon(ext, [int_1, int_2]) p1 = factories.orient(p, True) assert list(p1.exterior.coords) == ext[::-1] interiors = list(p1.interiors) assert list(interiors[0].coords) == int_1[::-1] assert list(interiors[1].coords) == int_2[::-1] def test_orient_unchanged() -> None: exterior = ((0, 0), (2, 0), (2, 2), (0, 2), (0, 0)) interiors = [ ((0.5, 0.25), (0.5, 1.25), (1.5, 1.25), (1.5, 0.25), (0.5, 0.25)), ((0.5, 1.25), (0.5, 1.75), (1, 1.75), (1, 1.25), (0.5, 1.25)), ] p = geometry.Polygon(exterior, interiors) p1 = factories.orient(p, True) assert p1.exterior.coords == exterior new_interiors = list(p1.interiors) assert new_interiors[0].coords == interiors[0] assert new_interiors[1].coords == interiors[1] def test_orient_false() -> None: exterior = ((0, 0), (2, 0), (2, 2), (0, 2), (0, 0)) interiors = [ ((0.5, 0.25), (0.5, 1.25), (1.5, 1.25), (1.5, 0.25), (0.5, 0.25)), ((0.5, 1.25), (0.5, 1.75), (1, 1.75), (1, 1.25), (0.5, 1.25)), ] p = geometry.Polygon(exterior, interiors) p1 = factories.orient(p, False) assert p1.exterior.coords == exterior[::-1] new_interiors = list(p1.interiors) assert new_interiors[0].coords == interiors[0][::-1] assert new_interiors[1].coords == interiors[1][::-1] def test_box() -> None: poly = factories.box(1, 2, 3, 4) assert poly.__geo_interface__ == { "type": "Polygon", "bbox": (1, 2, 3, 4), "coordinates": (((3, 2), (3, 4), (1, 4), (1, 2), (3, 2)),), } def test_box_cw() -> None: poly = factories.box(1, 2, 3, 4, ccw=False) assert poly.__geo_interface__ == { "type": "Polygon", "bbox": (1, 2, 3, 4), "coordinates": (((1, 2), (1, 4), (3, 4), (3, 2), (1, 2)),), } def test_shell_holes_from_wkt_coords() -> None: shell, holes = factories._shell_holes_from_wkt_coords( [ ["0 0", "10 20", "30 40", "0 0"], # type: ignore ], ) assert holes is None assert shell == [(0.0, 0.0), (10.0, 20.0), (30.0, 40.0), (0.0, 0.0)] class TestWKT: # valid and supported WKTs wkt_ok = [ "POINT(6 10)", "POINT M (1 1 80)", "LINESTRING(3 4,10 50,20 25)", "LINESTRING (30 10, 10 30, 40 40)", "MULTIPOLYGON (((10 10, 10 20, 20 20, 20 15, 10 10))," "((60 60, 70 70, 80 60, 60 60 )))", """MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)))""", """MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))""", "MULTIPOLYGON (((0 0,10 0,10 10,0 10,0 0)),((5 5,7 5,7 7,5 7, 5 5)))", "GEOMETRYCOLLECTION (POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))", ] # these are valid WKTs but not supported wkt_fail = [ "POINT ZM (1 1 5 60)", "POINT EMPTY", "MULTIPOLYGON EMPTY", "TIN (((0 0 0, 0 0 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 0 0 0)))", ] def test_point(self) -> None: p = factories.from_wkt("POINT (0.0 1.0)") assert isinstance(p, geometry.Point) assert p.x == 0.0 assert p.y == 1.0 assert p.wkt == "POINT (0 1)" assert str(p) == "POINT (0 1)" assert p.geom_type == "Point" def test_point_capitalized(self) -> None: pts = ["POINT (1 0)", "point (1 0)", "Point(1 0)", "pOinT(1 0)"] for pt in pts: assert factories.from_wkt(pt) == geometry.Point(1, 0) def test_linestring(self) -> None: line = factories.from_wkt( "LINESTRING(-72.991 46.177,-73.079 46.16," "-73.146 46.124,-73.177 46.071,-73.164 46.044)", ) assert isinstance(line, geometry.LineString) assert ( line.wkt == "LINESTRING (-72.991 46.177, " "-73.079 46.16, -73.146 46.124, " "-73.177 46.071, -73.164 46.044)" ) def test_linearring(self) -> None: r = factories.from_wkt("LINEARRING (0 0,0 1,1 0,0 0)") assert isinstance(r, geometry.LinearRing) assert r.wkt == "LINEARRING (0 0, 0 1, 1 0, 0 0)" def test_polygon(self) -> None: p = factories.from_wkt( "POLYGON((-91.611 76.227,-91.543 76.217," "-91.503 76.222,-91.483 76.221,-91.474 76.211," "-91.484 76.197,-91.512 76.193,-91.624 76.2," "-91.638 76.202,-91.647 76.211,-91.648 76.218," "-91.643 76.221,-91.636 76.222,-91.611 76.227))", ) assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0][0] == -91.611 assert p.exterior.coords[0] == p.exterior.coords[-1] assert len(p.exterior.coords) == 14 def test_polygon_1(self) -> None: p = factories.from_wkt( "POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2))", ) assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0] == p.exterior.coords[-1] assert p.exterior.coords[0] == (1.0, 1.0) assert len(list(p.interiors)) == 1 assert next(iter(p.interiors)).coords == ( (2.0, 2.0), (3.0, 2.0), (3.0, 3.0), (2.0, 3.0), (2.0, 2.0), ) assert ( p.wkt == "POLYGON ((1 1, 5 1, 5 5, " "1 5, 1 1),(2 2, 3 2, " "3 3, 2 3, 2 2))" ) def test_polygon_2(self) -> None: p = factories.from_wkt("POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))") assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0] == p.exterior.coords[-1] def test_polygon_3(self) -> None: p = factories.from_wkt( """POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10), (20 30, 35 35, 30 20, 20 30))""", ) assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0] == p.exterior.coords[-1] def test_multipoint(self) -> None: p = factories.from_wkt("MULTIPOINT(3.5 5.6,4.8 10.5)") assert isinstance(p, geometry.MultiPoint) assert next(iter(p.geoms)).x == 3.5 assert list(p.geoms)[1].y == 10.5 assert p.wkt == "MULTIPOINT (3.5 5.6, 4.8 10.5)" p = factories.from_wkt("MULTIPOINT ((10 40), (40 30), (20 20), (30 10))") assert isinstance(p, geometry.MultiPoint) assert next(iter(p.geoms)).x == 10.0 assert list(p.geoms)[3].y == 10.0 p = factories.from_wkt("MULTIPOINT (10 40, 40 30, 20 20, 30 10)") assert isinstance(p, geometry.MultiPoint) assert next(iter(p.geoms)).x == 10.0 assert list(p.geoms)[3].y == 10.0 def test_multilinestring(self) -> None: p = factories.from_wkt( "MULTILINESTRING ((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))", ) assert isinstance(p, geometry.MultiLineString) assert next(iter(p.geoms)).coords == (((3, 4), (10, 50), (20, 25))) assert list(p.geoms)[1].coords == (((-5, -8), (-10, -8), (-15, -4))) assert ( p.wkt == "MULTILINESTRING ((3 4, 10 50, " "20 25),(-5 -8, " "-10 -8, -15 -4))" ) def test_multilinestring_1(self) -> None: p = factories.from_wkt( """MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))""", ) assert isinstance(p, geometry.MultiLineString) assert p.wkt == ( "MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))" ) def test_multipolygon(self) -> None: p = factories.from_wkt( "MULTIPOLYGON (((0 0,10 20,30 40,0 0)," "(1 1,2 2,3 3,1 1))," "((100 100,110 110,120 120,100 100)))", ) assert isinstance(p, geometry.MultiPolygon) # two polygons: the first one has an interior ring assert len(list(p.geoms)) == 2 assert next(iter(p.geoms)).exterior.coords == ( (0.0, 0.0), (10.0, 20.0), (30.0, 40.0), (0.0, 0.0), ) assert next(iter(next(iter(p.geoms)).interiors)).coords == ( (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (1.0, 1.0), ) assert list(p.geoms)[1].exterior.coords == ( (100.0, 100.0), (110.0, 110.0), (120.0, 120.0), (100.0, 100.0), ) assert ( p.wkt == "MULTIPOLYGON (((0 0, 10 20, " "30 40, 0 0)," "(1 1, 2 2, 3 3, 1 1))," "((100 100, 110 110," " 120 120, 100 100)))" ) def test_multipolygon_1(self) -> None: p = factories.from_wkt( "MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1)," "(2 2, 3 2, 3 3, 2 3,2 2)),((3 3,6 2,6 4,3 3)))", ) assert isinstance(p, geometry.MultiPolygon) assert len(list(p.geoms)) == 2 def test_multipolygon_2(self) -> None: p = factories.from_wkt( "MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20))," "((15 5, 40 10, 10 20, 5 10, 15 5)))", ) assert isinstance(p, geometry.MultiPolygon) assert p.__geo_interface__ == { "type": "MultiPolygon", "bbox": (5.0, 5.0, 45.0, 40.0), "coordinates": ( (((30.0, 20.0), (10.0, 40.0), (45.0, 40.0), (30.0, 20.0)),), ( ( (15.0, 5.0), (40.0, 10.0), (10.0, 20.0), (5.0, 10.0), (15.0, 5.0), ), ), ), } def test_geometrycollection(self) -> None: gc = factories.from_wkt( "GEOMETRYCOLLECTION(POINT(4 6), LINESTRING(4 6,7 10))", ) assert isinstance(gc, geometry.GeometryCollection) assert len(list(gc.geoms)) == 2 assert isinstance(next(iter(gc.geoms)), geometry.Point) assert isinstance(list(gc.geoms)[1], geometry.LineString) assert gc.wkt == "GEOMETRYCOLLECTION (POINT (4 6), LINESTRING (4 6, 7 10))" def test_wkt_ok(self) -> None: for wkt in self.wkt_ok: factories.from_wkt(wkt) def test_wkt_fail(self) -> None: for wkt in self.wkt_fail: pytest.raises(factories.WKTParserError, factories.from_wkt, wkt) def test_wkt_tin(self) -> None: tin = "TIN (((0 0 0, 0 0 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 0 0 0)))" pytest.raises(factories.WKTParserError, factories.from_wkt, tin) class TestAsShape: def test_point(self) -> None: f = geometry.Point(0, 1) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ def test_point_00(self) -> None: f = geometry.Point(0, 0) assert f assert f == factories.shape(f) def test_linestring(self) -> None: f = geometry.LineString([(0, 0), (1, 1)]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ def test_linearring(self) -> None: f = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ def test_polygon(self) -> None: f = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] f = geometry.Polygon(e, [i]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int_1 = [(0.5, 0.25), (1.5, 0.25), (1.5, 1.25), (0.5, 1.25), (0.5, 0.25)] int_2 = [(0.5, 1.25), (1, 1.25), (1, 1.75), (0.5, 1.75), (0.5, 1.25)] f = geometry.Polygon(ext, [int_1, int_2]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ def test_multipoint(self) -> None: f = geometry.MultiPoint([[0.0, 0.0], [1.0, 2.0]]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ def test_multilinestring(self) -> None: f = geometry.MultiLineString([[[0.0, 0.0], [1.0, 2.0]]]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ assert f.bounds == (0, 0, 1, 2) def test_multipolygon(self) -> None: f = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], ), ], ) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ def test_geometrycollection(self) -> None: p = geometry.Point(0, 1) line = geometry.LineString([(0, 0), (1, 1)]) f = geometry.GeometryCollection([p, line]) s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ assert f.__geo_interface__["geometries"][0] == p.__geo_interface__ assert f.__geo_interface__["geometries"][1] == line.__geo_interface__ def test_nongeo(self) -> None: pytest.raises(AttributeError, factories.shape, "a") def test_empty_dict(self) -> None: pytest.raises(TypeError, factories.shape, {}) def test_notimplemented_interface(self) -> None: f = {"type": "Tin", "geometries": (1, 2, 3)} pytest.raises(NotImplementedError, factories.shape, f) def test_dict_as_shape(self) -> None: f = geometry.MultiLineString([[[0.0, 0.0], [1.0, 2.0]]]) s = factories.shape(f.__geo_interface__) assert f.__geo_interface__ == s.__geo_interface__ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_feature.py0000644000175100001770000001575414600335465017013 0ustar00runnerdocker"""Test Feature and FeatureCollection.""" import unittest import pytest from pygeoif import feature from pygeoif import geometry class TestFeature: def setup_method(self) -> None: self.a = geometry.Polygon( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ) self.b = geometry.Polygon( ((0.0, 0.0), (0.0, 2.0), (2.0, 1.0), (2.0, 0.0), (0.0, 0.0)), ) self.f1 = feature.Feature(self.a) self.f2 = feature.Feature(self.b) self.f3 = feature.Feature(self.a, {}, feature_id="1") self.fc = feature.FeatureCollection([self.f1, self.f2]) def test_feature_eq(self) -> None: assert self.f1 == feature.Feature(self.a) assert self.f3 == feature.Feature(self.a, {}, feature_id="1") def test_feature_neq_no_geo_interface(self) -> None: assert self.f1 != object() def test_feature_neq_no_geo_interface_geometry(self) -> None: assert self.f1 != unittest.mock.Mock(__geo_interface__={}) def test_feature_neq_no_geo_interface_type(self) -> None: assert self.f1 != unittest.mock.Mock(__geo_interface__={"type": "foo"}) def test_feature_neq_no_geo_interface_features(self) -> None: assert self.f1 != unittest.mock.Mock(__geo_interface__={"type": "Feature"}) def test_feature(self) -> None: pytest.raises(TypeError, feature.Feature) assert self.f1.__geo_interface__ == { "type": "Feature", "bbox": (0.0, 0.0, 1.0, 1.0), "geometry": { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ), }, "properties": {}, } self.f1.properties["coords"] = {"cube": (0, 0, 0)} assert self.f1.__geo_interface__ == { "type": "Feature", "bbox": (0.0, 0.0, 1.0, 1.0), "geometry": { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ), }, "properties": {"coords": {"cube": (0, 0, 0)}}, } assert self.f1.geometry.bounds == (0.0, 0.0, 1.0, 1.0) del self.f1.properties["coords"] def test_feature_with_id(self) -> None: assert self.f3.id == "1" assert self.f3.__geo_interface__ == { "type": "Feature", "bbox": (0.0, 0.0, 1.0, 1.0), "geometry": { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ), }, "id": "1", "properties": {}, } def test_feature_repr(self) -> None: assert ( repr(self.f3) == "Feature(" "Polygon(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),)," " {}, '1')" ) def test_feature_repr_eval(self) -> None: assert ( eval( repr(self.f2), {}, {"Polygon": geometry.Polygon, "Feature": feature.Feature}, ).__geo_interface__ == self.f2.__geo_interface__ ) def test_featurecollection(self) -> None: pytest.raises(TypeError, feature.FeatureCollection) pytest.raises(TypeError, feature.FeatureCollection, None) assert len(list(self.fc.features)) == 2 assert len(self.fc) == 2 assert self.fc.bounds == (0.0, 0.0, 2.0, 2.0) assert [self.f1, self.f2] == list(self.fc) assert self.fc.__geo_interface__ == { "bbox": (0.0, 0.0, 2.0, 2.0), "features": ( { "bbox": (0.0, 0.0, 1.0, 1.0), "geometry": { "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": ( ( (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0), ), ), "type": "Polygon", }, "properties": {}, "type": "Feature", }, { "bbox": (0.0, 0.0, 2.0, 2.0), "geometry": { "bbox": (0.0, 0.0, 2.0, 2.0), "coordinates": ( ( (0.0, 0.0), (0.0, 2.0), (2.0, 1.0), (2.0, 0.0), (0.0, 0.0), ), ), "type": "Polygon", }, "properties": {}, "type": "Feature", }, ), "type": "FeatureCollection", } def test_featurecollection_eq(self) -> None: assert self.fc == feature.FeatureCollection([self.f1, self.f2]) def test_featurecollection_neq_no_geo_interface(self) -> None: assert self.fc != object() def test_featurecollection_neq_no_geo_interface_geometry(self) -> None: assert self.fc != unittest.mock.Mock(__geo_interface__={}) def test_featurecollection_neq_no_geo_interface_features(self) -> None: assert self.fc != unittest.mock.Mock( __geo_interface__={"type": "FeatureCollection"}, ) def test_featurecollection_neq_no_geo_interface_len_features(self) -> None: assert self.fc != feature.FeatureCollection([self.f1]) def test_featurecollection_repr(self) -> None: assert ( repr(self.fc) == "FeatureCollection(" "(Feature(" "Polygon(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),)," " {}, None), " "Feature(" "Polygon(((0.0, 0.0), (0.0, 2.0), (2.0, 1.0), (2.0, 0.0), (0.0, 0.0)),), " "{}, None)))" ) def test_featurecollection_repr_eval(self) -> None: assert ( eval( repr(self.fc), {}, { "Polygon": geometry.Polygon, "Feature": feature.Feature, "FeatureCollection": feature.FeatureCollection, }, ).__geo_interface__ == self.fc.__geo_interface__ ) def test_featurecollection_bounds(self) -> None: ls1 = geometry.LineString(((0, 1), (1, 1))) ls2 = geometry.LineString(((2, 3), (3, 4))) fc = feature.FeatureCollection([feature.Feature(ls1), feature.Feature(ls2)]) assert fc.bounds == (0, 1, 3, 4) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_functions.py0000644000175100001770000003046214600335465017361 0ustar00runnerdocker"""Test geometric functions.""" import itertools import math import random from typing import Tuple import pytest from pygeoif.functions import centroid from pygeoif.functions import compare_coordinates from pygeoif.functions import compare_geo_interface from pygeoif.functions import convex_hull from pygeoif.functions import dedupe from pygeoif.functions import signed_area def circle_ish(x, y, r, steps): pts = [] steps = max(steps, 3) for step in range(steps): phi = 2 * math.pi * step / steps pts.append((r * math.cos(phi) + x, r * math.sin(phi) + y)) pts.append((x + r, y)) return pts def star_ish(x, y, r, steps): pts = [] steps = max(steps, 3) for step in range(steps): phi = 2 * math.pi * step / steps pts.append( (random.randrange(1, r + 1) * math.cos(phi) + x, r * math.sin(phi) + y), ) pts.append((x + r, y)) return pts def spiral_ish(x, y, r, steps): pts = [] for step in range(1, steps): phi = math.pi * step / steps pts.append((step * r * math.cos(phi) + x, step * r * math.sin(phi) + y)) pts.append((x + r, y)) return pts def crescent_ish(x, y, r, steps): pts = [] for step in range(1, steps): phi = math.pi * step / steps pts.append((step * r * 2 * math.cos(phi) + x, step * r * math.sin(phi) + y)) for step in range(steps, 0, -1): phi = math.pi * step / steps pts.append((step * r * math.cos(phi) + x, step * r * math.sin(phi) + y)) pts.append(pts[0]) return pts def test_signed_area() -> None: a0 = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] a1 = [(0, 0, 1), (0, 2, 2), (2, 2, 3), (2, 0, 4), (0, 0, 1)] assert signed_area(a0) == signed_area(a1) == -4 assert centroid(a0)[1] == centroid(a1)[1] == -4 def test_signed_area2() -> None: a0 = [(0, 0), (0, 1), (1, 1), (0, 0)] assert centroid(a0)[1] == signed_area(a0) def test_centroid_line() -> None: a0 = [(0, 0), (1, 1), (0, 0)] with pytest.raises(ZeroDivisionError): assert centroid(a0) def test_signed_area_0_3d() -> None: assert signed_area(((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))) == 0.0 def test_signed_area_0_2d() -> None: assert signed_area(((0.0, 0.0), (0.0, 0.0), (0.0, 0.0))) == 0.0 def test_signed_area_circle_ish() -> None: for i in range(100): x = random.randrange(20) y = random.randrange(20) r = random.randrange(1, 20 + i) pts = [] steps = random.randrange(3, 30 + i) pts = circle_ish(x, y, r, steps) center1, area1 = centroid(pts) center2, area2 = centroid(list(reversed(pts))) # both area computations should be approximately equal assert math.isclose(area1, signed_area(pts)) assert math.isclose(area2, signed_area(list(reversed(pts)))) assert center1, area1 == (center2, area2) assert math.isclose(center1[0], x, abs_tol=0.000_000_1) assert math.isclose(center1[1], y, abs_tol=0.000_000_1) # we are computing an approximation of math.pi if steps > 12: assert 3.0 * r**2 < area1 < 3.2 * r**2 if steps > 30: assert 3.1 * r**2 < area1 < 3.2 * r**2 def test_signed_area_crescent_ish() -> None: for i in range(100): x = random.randrange(20) - i y = random.randrange(20 + i) r = random.randrange(1, 20) pts = [] steps = random.randrange(4, 20) pts = crescent_ish(x, y, r, steps) center1, area1 = centroid(pts) center2, area2 = centroid(list(reversed(pts))) assert math.isclose(area1, signed_area(pts)) assert math.isclose(area2, signed_area(list(reversed(pts)))) assert center1, area1 == (center2, area2) def test_empty_hull() -> None: assert convex_hull([]) == [] def test_point() -> None: pts = [(0, 0)] hull = convex_hull(pts) assert hull == [(0, 0)] def test_line() -> None: pts = [(0, 0), (1, 1)] hull = convex_hull(pts) assert hull == [(0, 0), (1, 1)] def test_line_minimal() -> None: pts = [(0, 0), (1, 1), (1, 0)] hull = convex_hull(pts) assert hull == ((0, 0), (1, 0), (1, 1), (0, 0)) def test_line2() -> None: pts = ((x, x) for x in range(5)) hull = convex_hull(pts) assert hull == [(0, 0), (4, 4)] def test_line3() -> None: pts = ((x, x) for x in range(3)) hull = convex_hull(pts) assert hull == [(0, 0), (2, 2)] def test_square() -> None: pts = list(itertools.product(range(100), range(100))) hull = convex_hull(pts) assert hull == ((0, 0), (99, 0), (99, 99), (0, 99), (0, 0)) def test_triangle() -> None: pts = [] for x in range(100): pts.extend((x, y) for y in range(x + 1)) hull = convex_hull(pts) assert hull == ((0, 0), (99, 0), (99, 99), (0, 0)) def test_trapezoid() -> None: pts = [] for x in range(100): pts.extend((x, y) for y in range(-x - 1, x + 1)) hull = convex_hull(pts) assert hull == ((0, -1), (99, -100), (99, 99), (0, 0), (0, -1)) def test_circles() -> None: for _ in range(10): x = random.randrange(20) y = random.randrange(20) r = random.randrange(1, 20) steps = random.randrange(4, 20) pts = circle_ish(x, y, r, steps) hull = convex_hull(pts) assert set(hull) == set(pts) assert len(hull) == len(pts) def test_spiral() -> None: for _ in range(10): x = random.randrange(20) y = random.randrange(20) pts = [] steps = random.randrange(4, 20) spiral_ish(x, y, 1, steps) hull = convex_hull(pts) assert set(hull) == set(pts) assert len(hull) == len(pts) def test_crescent() -> None: for _ in range(10): x = random.randrange(20) y = random.randrange(20) pts = [] steps = random.randrange(4, 20) crescent_ish(x, y, 1, steps) hull = convex_hull(pts) assert len(hull) == len(pts) / 2 def test_star() -> None: for _ in range(10): x = random.randrange(20) y = random.randrange(20) pts = [] steps = random.randrange(4, 20) star_ish(x, y, 1, steps) hull = convex_hull(pts) assert set(hull).issubset(set(pts)) assert len(hull) <= len(pts) def test_random() -> None: """The convex hull of an exiting hull must be the same as the hull itself.""" for i in range(100): pts = ( (random.randrange(-x, x + 1), random.randrange(-x, x + 1)) for x in range(i + 1) ) hull = convex_hull(pts) assert convex_hull(hull) == hull if len(hull) > 3: _, area = centroid(tuple(hull)) assert math.isclose(area, signed_area(hull)) def test_dedupe_point() -> None: assert dedupe(((1, 2, 3),) * 10) == ((1, 2, 3),) def test_dedupe_line() -> None: assert dedupe(((1, 2, 3), (4, 5, 6)) * 3) == ( (1, 2, 3), (4, 5, 6), (1, 2, 3), (4, 5, 6), (1, 2, 3), (4, 5, 6), ) def test_dedupe_line2() -> None: assert dedupe(((1, 2, 3),) * 2 + ((4, 5, 6),) * 3) == ((1, 2, 3), (4, 5, 6)) @pytest.mark.parametrize( ("numbers", "expected"), [ ((0, 0), True), ((0, 1), False), ((1, 0), False), ((1, 1), True), ((0.3, 0.2 + 0.1), True), ((1, "1"), False), (("10", 10), False), ((None, 1), False), ((1, None), False), ((None, None), False), ((1, 1.0), True), ((1.0, 1), True), ((math.inf, math.inf), True), ((-math.inf, -math.inf), True), ((math.inf, -math.inf), False), ((math.nan, math.nan), False), ], ) def test_compare_numbers(numbers: Tuple[float, float], expected: bool) -> None: """Compare numbers for equality.""" assert compare_coordinates(*numbers) is expected @pytest.mark.parametrize( ("points", "expected"), [ (((1, 2), [1, 2]), True), (((1, 2), [1, 3]), False), (((1, 2), [2, 2]), False), (((1, 2, 0), (1, 2)), False), (((1, 2), (1, 2, 0)), False), (((1, 2, 0), [1, 2, 0]), True), (((1, 2, 0), [1, 2, 1]), False), (((1, 2, 0), [1, 3, 0]), False), (((1, 2, 0), [2, 2, 0]), False), (((0.3, 0.3), (0.1 + 0.2, 0.1 + 0.2)), True), (((0.3, 0.3), ("0.3", "0.3")), False), ], ) def test_compare_points(points, expected: bool) -> None: """Compare a single set of coordinates.""" assert compare_coordinates(*points) is expected @pytest.mark.parametrize( ("lines", "expected"), [ ((((1, 2), (3, 4)), ((1, 2), (3, 4))), True), ((((1, 2), (3, 4)), [[1, 2], [3, 4]]), True), ((((1, 2), (3, 4)), ((1, 2), (3, 5))), False), ((((1, 2), (3, (3, 5))), ((1, 2), (3, (3, 5)))), True), ((((1, 2), (3, 4)), ((1, 2), (3, 4), (3, 4))), False), ], ) def test_compare_lines(lines, expected: bool) -> None: """Compare a sequence of coordinates.""" assert compare_coordinates(*lines) is expected @pytest.mark.parametrize( ("polygons", "expected"), [ ( ( (((1, 2), (3, 4)), ((5, 6), (7, 8))), (((1, 2), (3, 4)), ((5, 6), (7, 8))), ), True, ), ( ( (((1, 2), (3, 4)), ((5, 6), (7, 8))), (((1, 2), (3, 4)), ((5, 6), (7, 8)), ((5, 6), (7, 8))), ), False, ), ( ( (((1, 2), (3, 4)), ((5, 6), (7, 8))), (((1, 2), (3, 4)), (((5, 6), (7, 8)), ((5, 6), (7, 8)))), ), False, ), ( ( [[[1, 2], [3, 4]], ([[5, 6], (7, 8)], ([5, 6], [7, 8]))], (((1, 2), (3, 4)), (((5, 6), (7, 8)), ((5, 6), (7, 8)))), ), True, ), ], ) def test_compare_polygons(polygons, expected: bool) -> None: """Compare nested sequences of coordinates.""" assert compare_coordinates(*polygons) is expected def test_compare_eq_geo_interface() -> None: geo_if = { "geometries": ( { "geometries": ( { "geometries": ( { "bbox": (0, 0, 0, 0), "coordinates": (0, 0), "type": "Point", }, { "bbox": (0, 0, 2, 2), "coordinates": ((0, 0), (1, 1), (1, 2), (2, 2)), "type": "MultiPoint", }, ), "type": "GeometryCollection", }, { "bbox": (0, 0, 3, 1), "coordinates": ((0, 0), (3, 1)), "type": "LineString", }, ), "type": "GeometryCollection", }, {"coordinates": (((0, 0), (1, 1), (1, 0), (0, 0)),), "type": "Polygon"}, { "bbox": (0, 0, 2, 2), "coordinates": ( ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), ), "type": "Polygon", }, {"coordinates": (0, 0), "type": "Point"}, {"bbox": (-1, -1, -1, -1), "coordinates": (-1, -1), "type": "Point"}, {"coordinates": ((0, 0), (1, 1), (1, 0), (0, 0)), "type": "LinearRing"}, { "bbox": (0, 0, 1, 1), "coordinates": ((0, 0), (1, 1)), "type": "LineString", }, ), "type": "GeometryCollection", } assert compare_geo_interface(geo_if, geo_if) is True def test_compare_neq_geo_interface() -> None: geo_if1 = { "type": "Point", "bbox": (0, 1, 0, 1), "coordinates": (0.0, 1.0, 2.0), } geo_if2 = { "coordinates": (0.0, 1.0, 3.0), } assert compare_geo_interface(geo_if1, geo_if2) is False def test_compare_neq_empty_geo_interface() -> None: geo_if = { "type": "Point", "bbox": (0, 1, 0, 1), "coordinates": (0.0, 1.0, 2.0), } assert compare_geo_interface(geo_if, {}) is False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_geometrycollection.py0000644000175100001770000003767214600335465021272 0ustar00runnerdocker"""Test Baseclass.""" import pytest from pygeoif import geometry from pygeoif.factories import from_wkt def test_geo_interface() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing( [ (0, 0), (1, 1), (1, 0), (0, 0), ], ) line = geometry.LineString( [ (0, 0), (1, 1), ], ) gc = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) assert gc.__geo_interface__ == { "geometries": ( { "bbox": (0, 0, 1, 1), "coordinates": (((0, 0), (1, 1), (1, 0), (0, 0)),), "type": "Polygon", }, { "bbox": (0, 0, 2, 2), "coordinates": ( ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), ), "type": "Polygon", }, {"bbox": (0, 0, 0, 0), "coordinates": (0, 0), "type": "Point"}, {"bbox": (-1, -1, -1, -1), "coordinates": (-1, -1), "type": "Point"}, { "bbox": (0, 0, 1, 1), "coordinates": ((0, 0), (1, 1), (1, 0), (0, 0)), "type": "LinearRing", }, { "bbox": (0, 0, 1, 1), "coordinates": ((0, 0), (1, 1)), "type": "LineString", }, ), "type": "GeometryCollection", } def test_geo_wkt() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) assert gc.wkt == ( "GEOMETRYCOLLECTION " "(POLYGON ((0 0, 1 1, 1 0, 0 0)), " "POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0),(1 0, 0.5 0.5, 1 1, 1.5 0.5, 1 0)), " "POINT (0 0), POINT (-1 -1), " "LINEARRING (0 0, 1 1, 1 0, 0 0), " "LINESTRING (0 0, 1 1))" ) def test_len() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) assert len(gc) == 6 def test_geoms() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) for k, v in zip(gc.geoms, [poly1, poly2, p0, p1, ring, line]): assert k == v def test_repr() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) assert repr(gc) == ( "GeometryCollection(" "(Polygon(((0, 0), (1, 1), (1, 0), (0, 0)),), " "Polygon(((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), " "(((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),)), " "Point(0, 0), Point(-1, -1), " "LinearRing(((0, 0), (1, 1), (1, 0), (0, 0))), " "LineString(((0, 0), (1, 1))))" ")" ) def test_repr_eval() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) assert ( eval( repr(gc), {}, { "LinearRing": geometry.LinearRing, "Polygon": geometry.Polygon, "Point": geometry.Point, "LineString": geometry.LineString, "GeometryCollection": geometry.GeometryCollection, }, ).__geo_interface__ == gc.__geo_interface__ ) def test_eq() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc1 = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) gc2 = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) assert gc1 == gc2 def test_neq_len() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc1 = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) gc2 = geometry.GeometryCollection([poly1, poly2, p0, p1, ring]) assert gc1 != gc2 def test_neq_sort() -> None: poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc1 = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) gc2 = geometry.GeometryCollection([poly1, poly2, p0, p1, line, line]) assert gc1 != gc2 def test_neq_type() -> None: line = geometry.LineString([(0, 0), (1, 1)]) gc1 = geometry.GeometryCollection([line]) assert gc1 != line def test_neq_coords() -> None: p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) p2 = geometry.Point(-1, -2) gc1 = geometry.GeometryCollection([p0, p1]) gc2 = geometry.GeometryCollection([p0, p2]) assert gc1 != gc2 def test_neq_interface() -> None: line = geometry.LineString([(0, 0), (1, 1)]) gc1 = geometry.GeometryCollection([line]) assert gc1 != object() def test_convex_hull() -> None: p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) p2 = geometry.Point(-1, -2) line = geometry.LineString([(0, 0), (3, 1)]) poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) gc = geometry.GeometryCollection([p0, p1, p2, line, poly1, poly2]) assert gc.convex_hull == geometry.Polygon( ((-1, -2), (2, 0), (3, 1), (2, 2), (0, 2), (-1, -1), (-1, -2)), ) def test_is_empty() -> None: gc = geometry.GeometryCollection([]) assert gc.is_empty def test_empty_wkt() -> None: gc = geometry.GeometryCollection([]) assert gc.wkt == "GEOMETRYCOLLECTION EMPTY" def test_repr_empty() -> None: gc = geometry.GeometryCollection([]) assert repr(gc) == "GeometryCollection(())" def test_empty_bounds() -> None: gc = geometry.GeometryCollection([]) assert gc.bounds == () def test_multipoint_wkt() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc = geometry.GeometryCollection([multipoint]) assert gc.wkt == "GEOMETRYCOLLECTION (MULTIPOINT (0 0, 1 1, 1 2, 2 2))" def test_multipoint_repr() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc = geometry.GeometryCollection([multipoint]) assert ( repr(gc) == "GeometryCollection((MultiPoint(((0, 0), (1, 1), (1, 2), (2, 2))),))" ) def test_multipoint_geo_interface() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc = geometry.GeometryCollection([multipoint]) assert gc.__geo_interface__ == { "type": "GeometryCollection", "geometries": ( { "type": "MultiPoint", "coordinates": ((0, 0), (1, 1), (1, 2), (2, 2)), "bbox": (0, 0, 2, 2), }, ), } def test_nested_geometry_collection() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) line = geometry.LineString([(0, 0), (3, 1)]) gc2 = geometry.GeometryCollection([gc1, line]) poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) gc3 = geometry.GeometryCollection([gc2, poly1]) assert gc3.wkt == ( "GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (" "POINT (0 0), MULTIPOINT (0 0, 1 1, 1 2, 2 2)), LINESTRING (0 0, 3 1)), " "POLYGON ((0 0, 1 1, 1 0, 0 0)))" ) def test_nested_geometry_collection_geo_interface() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) line = geometry.LineString([(0, 0), (3, 1)]) gc2 = geometry.GeometryCollection([gc1, line]) poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) gc3 = geometry.GeometryCollection([gc2, poly1]) assert gc3.__geo_interface__ == { "geometries": ( { "geometries": ( { "geometries": ( { "bbox": (0, 0, 0, 0), "coordinates": (0, 0), "type": "Point", }, { "bbox": (0, 0, 2, 2), "coordinates": ((0, 0), (1, 1), (1, 2), (2, 2)), "type": "MultiPoint", }, ), "type": "GeometryCollection", }, { "bbox": (0, 0, 3, 1), "coordinates": ((0, 0), (3, 1)), "type": "LineString", }, ), "type": "GeometryCollection", }, { "bbox": (0, 0, 1, 1), "coordinates": (((0, 0), (1, 1), (1, 0), (0, 0)),), "type": "Polygon", }, ), "type": "GeometryCollection", } def test_nested_geometry_collection_eq() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) line = geometry.LineString([(0, 0), (3, 1)]) gc2 = geometry.GeometryCollection([gc1, line]) poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) gc3 = geometry.GeometryCollection([gc2, poly1]) gc4 = geometry.GeometryCollection([gc2, poly1]) assert gc3 == gc4 def test_nested_geometry_collection_neq() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) gc1_1 = geometry.GeometryCollection( [geometry.Point(0, 0), multipoint, geometry.Point(0, 0)], ) line = geometry.LineString([(0, 0), (3, 1)]) gc2 = geometry.GeometryCollection([gc1, line]) gc2_1 = geometry.GeometryCollection([gc1_1, line]) poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) gc3 = geometry.GeometryCollection([gc2, poly1]) gc4 = geometry.GeometryCollection([gc2_1, poly1]) assert gc3 != gc4 def test_geometry_collection_neq_when_empty() -> None: gc1 = geometry.GeometryCollection([]) gc2 = geometry.GeometryCollection([geometry.Point(0, 0)]) assert gc1 != gc2 assert gc2 != gc1 assert gc1 != gc1 # noqa: PLR0124 def test_nested_geometry_collection_repr_eval() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) line1 = geometry.LineString([(0, 0), (3, 1)]) gc2 = geometry.GeometryCollection([gc1, line1]) poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] poly2 = geometry.Polygon(e, [i]) p0 = geometry.Point(0, 0) p1 = geometry.Point(-1, -1) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) line = geometry.LineString([(0, 0), (1, 1)]) gc = geometry.GeometryCollection([gc2, poly1, poly2, p0, p1, ring, line]) assert ( eval( repr(gc), {}, { "LinearRing": geometry.LinearRing, "Polygon": geometry.Polygon, "Point": geometry.Point, "LineString": geometry.LineString, "GeometryCollection": geometry.GeometryCollection, "MultiPoint": geometry.MultiPoint, }, ).__geo_interface__ == gc.__geo_interface__ ) def test_multipoint_collection_wkt_roundtrip() -> None: gc = geometry.GeometryCollection((geometry.MultiPoint(((0.0, 0.0),)),)) assert from_wkt(str(gc)) == gc def test_multi_geometry_collection_wkt() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) line = geometry.LineString([(0, 0), (3, 1)]) lines = geometry.MultiLineString( ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), ) polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), (((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1)),), ), (((0, 0), (0, 1), (1, 1), (1, 0)),), ( ((0, 0), (0, 1), (1, 1), (1, 0)), (((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1)),), ), (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),), ], unique=True, ) ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) gc = geometry.GeometryCollection([multipoint, line, lines, polys, ring]) assert from_wkt(str(gc)) == gc @pytest.mark.xfail(reason="WKT parsing for nested GeometryCollections not implemented.") def test_nested_geometry_collections_wkt() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) line = geometry.LineString([(0, 0), (3, 1)]) gc2 = geometry.GeometryCollection([gc1, line]) poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) gc3 = geometry.GeometryCollection([gc2, poly1]) assert from_wkt(str(gc3)) == gc3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_line.py0000644000175100001770000001214714600335465016300 0ustar00runnerdocker"""Test LineString.""" from unittest import mock import pytest from pygeoif import exceptions from pygeoif import geometry def test_coords_get2d() -> None: line = geometry.LineString([(0, 0), (1, 1)]) assert line.coords == ((0.0, 0.0), (1.0, 1.0)) def test_coords_get_3d() -> None: line = geometry.LineString([(0, 0, 0), (1, 1, 1)]) assert line.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1)) def test_empty_points_omitted() -> None: line = geometry.LineString([(0, 0, 0), (None, None, None), (2, 2, 2)]) assert line.coords == ((0, 0, 0), (2, 2, 2)) def test_set_geoms_raises() -> None: line = geometry.LineString([(0, 0), (1, 0)]) # pragma: no mutate with pytest.raises( exceptions.DimensionError, match="^All coordinates must have the same dimension$", ): line._set_geoms([(0.0, 0.0, 0), (1.0, 1.0)]) # pragma: no mutate def test_geo_interface() -> None: line = geometry.LineString([(0, 0), (1, 1)]) assert line.__geo_interface__ == { "type": "LineString", "bbox": (0.0, 0.0, 1.0, 1.0), # pragma: no mutate "coordinates": ((0.0, 0.0), (1.0, 1.0)), } def test_bounds() -> None: line = geometry.LineString([(0, 0), (1, 1)]) assert line.bounds == (0.0, 0.0, 1.0, 1.0) def test_bounds3d() -> None: line = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) # pragma: no mutate assert line.bounds == (0.0, 0.0, 2.0, 2.0) # pragma: no mutate def test_wkt() -> None: line = geometry.LineString([(0, 0), (1, 1), (2, 2)]) assert line.wkt == "LINESTRING (0 0, 1 1, 2 2)" def test_wkt3d() -> None: line = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) assert line.wkt == "LINESTRING Z (0 0 0, 1 1 3, 2 2 6)" def test_from_dict() -> None: line = geometry.LineString._from_dict( { "type": "LineString", "bbox": (0.0, 0.0, 1.0, 1.0), # pragma: no mutate "coordinates": ((0.0, 0.0), (1.0, 1.0)), }, ) assert line.coords == ((0.0, 0.0), (1.0, 1.0)) def test_from_compatible() -> None: not_a_geometry = mock.Mock( __geo_interface__={ "type": "LineString", "coordinates": ((0.0, 0.0, 1.0), (1.0, 1.0, 2.0)), }, ) line = geometry.LineString._from_interface(not_a_geometry) assert isinstance(line, geometry.LineString) assert line.coords == ((0.0, 0.0, 1.0), (1.0, 1.0, 2.0)) def test_repr2d() -> None: line = geometry.LineString([(0, 0), (1, 1), (2, 2)]) assert repr(line) == "LineString(((0, 0), (1, 1), (2, 2)))" def test_repr3d() -> None: line = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) assert repr(line) == "LineString(((0, 0, 0), (1, 1, 3), (2, 2, 6)))" def test_repr_eval() -> None: line = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) assert ( eval(repr(line), {}, {"LineString": geometry.LineString}).__geo_interface__ == line.__geo_interface__ ) def test_has_z_2d() -> None: line = geometry.LineString([(0, 0), (1, 1), (2, 2)]) assert not line.has_z def test_has_z_3d() -> None: line = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) assert line.has_z def test_from_points() -> None: p1 = geometry.Point(0, 0) p2 = geometry.Point(1, 1) line = geometry.LineString.from_points(p1, p2) assert line.coords == ((0, 0), (1, 1)) def test_from_points_3d() -> None: p1 = geometry.Point(0, 0, 1) p2 = geometry.Point(1, 1, 2) line = geometry.LineString.from_points(p1, p2) assert line.coords == ((0, 0, 1), (1, 1, 2)) def test_from_points_mixed() -> None: p1 = geometry.Point(0, 0, 1) p2 = geometry.Point(1, 1) with pytest.raises(exceptions.DimensionError): geometry.LineString.from_points(p1, p2) def test_convex_hull() -> None: line = geometry.LineString([(0, 0), (1, 1), (2, 2)]) assert line.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d() -> None: line = geometry.LineString([(0, 0, 0), (1, 1, 1), (2, 2, 2)]) assert line.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d_collapsed_to_point() -> None: line = geometry.LineString([(0, 0, 0), (0, 0, 1), (0, 0, 2)]) assert line.convex_hull == geometry.Point(0, 0) def test_convex_hull_linear_ring() -> None: line = geometry.LineString([(0, 0), (1, 0), (2, 2)]) assert line.convex_hull == geometry.Polygon([(0, 0), (1, 0), (2, 2), (0, 0)]) def test_convex_hull_empty() -> None: line = geometry.LineString([]) assert line.convex_hull is None def test_from_coordinates() -> None: line = geometry.LineString([(0, 0), (1, 0), (2, 2)]) assert geometry.LineString.from_coordinates(line.coords) == line def test_empty() -> None: line = geometry.LineString([]) assert line.is_empty def test_empty_1_pt() -> None: line = geometry.LineString([(0, 0)]) assert not line.is_empty def test_repr_empty() -> None: line = geometry.LineString([]) assert repr(line) == "LineString(())" def test_empty_bounds() -> None: line = geometry.LineString([]) assert line.bounds == () ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_linear_ring.py0000644000175100001770000001225414600335465017641 0ustar00runnerdocker"""Test LinearRing.""" from unittest import mock import pytest from pygeoif import exceptions from pygeoif import functions from pygeoif import geometry def test_coords_get2d() -> None: ring = geometry.LinearRing([(0, 0), (1, 1), (2, 0)]) assert ring.coords == ((0.0, 0.0), (1.0, 1.0), (2, 0), (0, 0)) def test_coords_get_3d() -> None: ring = geometry.LinearRing([(0, 0, 0), (1, 1, 1), (1, 2, 3)]) assert ring.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1), (1, 2, 3), (0, 0, 0)) def test_set_geoms_raises() -> None: ring = geometry.LinearRing([(0, 0), (1, 0)]) # pragma: no mutate with pytest.raises( exceptions.DimensionError, match="^All coordinates must have the same dimension$", ): ring._set_geoms([(0.0, 0.0, 0), (1.0, 1.0)]) # pragma: no mutate def test_geo_interface() -> None: ring = geometry.LinearRing([(0, 0), (1, 1), (2, 2)]) assert ring.__geo_interface__ == { "type": "LinearRing", "bbox": (0.0, 0.0, 2, 2), # pragma: no mutate "coordinates": ((0.0, 0.0), (1.0, 1.0), (2, 2), (0, 0)), } def test_bounds() -> None: ring = geometry.LinearRing([(1, 0), (3, 2)]) assert ring.bounds == (1.0, 0.0, 3.0, 2.0) def test_bounds3d() -> None: ring = geometry.LinearRing([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) # pragma: no mutate assert ring.bounds == (0.0, 0.0, 2.0, 2.0) # pragma: no mutate def test_wkt() -> None: ring = geometry.LinearRing([(0, 0), (1, 1), (2, 2)]) assert ring.wkt == "LINEARRING (0 0, 1 1, 2 2, 0 0)" def test_wkt3d() -> None: ring = geometry.LinearRing([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) assert ring.wkt == "LINEARRING Z (0 0 0, 1 1 3, 2 2 6, 0 0 0)" def test_from_dict() -> None: ring = geometry.LinearRing._from_dict( { "type": "LinearRing", "bbox": (0.0, 0.0, 1.0, 1.0), # pragma: no mutate "coordinates": ((0.0, 0.0), (1.0, 1.0), (1, 2)), }, ) assert ring.coords == ((0.0, 0.0), (1.0, 1.0), (1, 2), (0, 0)) def test_from_compatible() -> None: not_a_geometry = mock.Mock( __geo_interface__={ "type": "LinearRing", "coordinates": ((0.0, 0.0, 1.0), (1.0, 1.0, 2.0), (0, 4, 3)), }, ) ring = geometry.LinearRing._from_interface(not_a_geometry) assert isinstance(ring, geometry.LinearRing) assert ring.coords == ((0.0, 0.0, 1.0), (1.0, 1.0, 2.0), (0, 4, 3), (0, 0, 1)) def test_repr2d() -> None: ring = geometry.LinearRing([(0, 0), (1, 1), (2, 2)]) assert repr(ring) == "LinearRing(((0, 0), (1, 1), (2, 2), (0, 0)))" def test_repr3d() -> None: ring = geometry.LinearRing([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) assert repr(ring) == "LinearRing(((0, 0, 0), (1, 1, 3), (2, 2, 6), (0, 0, 0)))" def test_repr_eval() -> None: ring = geometry.LinearRing([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) assert ( eval(repr(ring), {}, {"LinearRing": geometry.LinearRing}).__geo_interface__ == ring.__geo_interface__ ) def test_signed_area() -> None: assert functions.signed_area(((0.0, 0.0), (1.0, 1.0), (2, 0), (0, 0))) == -1.0 assert functions.signed_area(((0, 0, 5), (1, 0, 6), (1, 1, 7), (0, 0, 5))) == 0.5 def test_from_points() -> None: p1 = geometry.Point(0, 0) p2 = geometry.Point(1, 1) p3 = geometry.Point(0, 1) ring = geometry.LinearRing.from_points(p1, p2, p3) assert ring.coords == ((0, 0), (1, 1), (0, 1), (0, 0)) def test_convex_hull() -> None: line = geometry.LinearRing([(0, 0), (1, 1), (2, 2)]) assert line.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d() -> None: line = geometry.LinearRing([(0, 0, 0), (1, 1, 1), (2, 2, 2)]) assert line.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d_collapsed_to_point() -> None: line = geometry.LinearRing([(0, 0, 0), (0, 0, 1), (0, 0, 2)]) assert line.convex_hull == geometry.Point(0, 0) def test_convex_hull_linear_ring() -> None: line = geometry.LinearRing([(0, 0), (1, 0), (2, 2)]) assert line.convex_hull == geometry.Polygon([(0, 0), (1, 0), (2, 2), (0, 0)]) def test_is_ccw() -> None: line = geometry.LinearRing([(0, 0), (1, 0), (1, 1), (0, 0)]) assert line.is_ccw def test_is_cw() -> None: line = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) assert not line.is_ccw def test_centroid_line() -> None: line = geometry.LinearRing([(0, 0), (0, 1)]) assert line.centroid is None def test_centroid_3d() -> None: line = geometry.LinearRing([(0, 0, 1), (2, 0, 2), (2, 2, 0), (0, 2, 0)]) with pytest.raises( exceptions.DimensionError, match="^Centeroid is only implemented for 2D coordinates$", ): assert line.centroid def test_centroid_crossing() -> None: line = geometry.LinearRing([(0, 0), (1, 0), (1, 1), (0, -1)]) assert line.centroid is None def test_centroid_valid() -> None: line = geometry.LinearRing([(0, 0), (4, 0), (4, 2), (0, 2)]) assert line.centroid == geometry.Point(2, 1) def test_empty() -> None: ring = geometry.LinearRing([]) assert ring.is_empty def test_empty_bounds() -> None: ring = geometry.LinearRing([]) assert ring.bounds == () ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_multiline.py0000644000175100001770000001254514600335465017355 0ustar00runnerdocker"""Test MultiLineString.""" from pygeoif import geometry def test_geoms() -> None: lines = geometry.MultiLineString( ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), ) for line in lines.geoms: assert type(line) is geometry.LineString def test_len() -> None: lines = geometry.MultiLineString( ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), ) assert len(lines) == 2 def test_geo_interface() -> None: lines = geometry.MultiLineString( ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), ) assert lines.__geo_interface__ == { "type": "MultiLineString", "bbox": (0, 0, 2, 2), "coordinates": (((0, 0), (1, 1), (1, 2), (2, 2)), ((0.0, 0.0), (1.0, 2.0))), } def test_from_dict() -> None: lines = geometry.MultiLineString._from_dict( { "type": "MultiLineString", "bbox": (0, 0, 2, 2), "coordinates": (((0, 0), (1, 1), (1, 2), (2, 2)), ((0.0, 0.0), (1.0, 2.0))), }, ) assert lines.__geo_interface__ == { "type": "MultiLineString", "bbox": (0, 0, 2, 2), "coordinates": (((0, 0), (1, 1), (1, 2), (2, 2)), ((0.0, 0.0), (1.0, 2.0))), } def test_wkt() -> None: lines = geometry.MultiLineString( ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), ) assert lines.wkt == "MULTILINESTRING ((0 0, 1 1, 1 2, 2 2),(0.0 0.0, 1.0 2.0))" def test_wkt_single_line() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 1), (1, 2), (2, 2)],)) assert lines.wkt == "MULTILINESTRING ((0 0, 1 1, 1 2, 2 2))" def test_repr() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])) assert ( repr(lines) == "MultiLineString((((0, 0), (1, 1)), ((0.0, 0.0), (1.0, 2.0))))" ) def test_repr_single_line() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 1), (1, 2), (2, 2)],)) assert repr(lines) == "MultiLineString((((0, 0), (1, 1), (1, 2), (2, 2)),))" def test_unique() -> None: lines = geometry.MultiLineString( ([(0, 0), (1, 1), (1, 2), (2, 2)], [(0, 0), (1.0, 1.0), (1.0, 2.0), (2, 2)]), unique=True, ) assert len(lines) == 1 def test_repr_eval() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])) assert ( eval( repr(lines), {}, {"MultiLineString": geometry.MultiLineString}, ).__geo_interface__ == lines.__geo_interface__ ) def test_repr_eval_single_line() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 1), (1, 2), (2, 2)],)) assert ( eval( repr(lines), {}, {"MultiLineString": geometry.MultiLineString}, ).__geo_interface__ == lines.__geo_interface__ ) def test_convex_hull() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [2.0, 2.0]])) assert lines.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d() -> None: lines = geometry.MultiLineString(([(0, 0, 0), (1, 1, 1)], [[0, 0, 1], [2, 2, 2]])) assert lines.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d_collapsed_to_point() -> None: lines = geometry.MultiLineString(([(0, 0, 0), (0, 0, 1)], [[0, 0, 2], [0, 0, 3]])) assert lines.convex_hull == geometry.Point(0, 0) def test_convex_hull_linear_ring() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 0)], [[1, 1], [2, 2]])) assert lines.convex_hull == geometry.Polygon([(0, 0), (1, 0), (2, 2), (0, 0)]) def test_from_linestrings() -> None: line1 = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) line2 = geometry.LineString([(0, 0), (1, 1), (2, 2)]) lines = geometry.MultiLineString.from_linestrings(line1, line2) assert lines == geometry.MultiLineString( (((0, 0, 0), (1, 1, 3), (2, 2, 6)), ((0, 0), (1, 1), (2, 2))), ) def test_from_linestrings_unique() -> None: line1 = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) line2 = geometry.LineString([(0, 0), (1, 1), (2, 2)]) lines = geometry.MultiLineString.from_linestrings( line1, line2, line1, line2, line1, unique=True, ) assert lines == geometry.MultiLineString( (((0, 0, 0), (1, 1, 3), (2, 2, 6)), ((0, 0), (1, 1), (2, 2))), unique=True, ) def test_from_linestrings_non_unique() -> None: line1 = geometry.LineString([(0, 0, 0), (1, 1, 3), (2, 2, 6)]) line2 = geometry.LineString([(0, 0), (1, 1), (2, 2)]) lines = geometry.MultiLineString.from_linestrings( line1, line2, line1, line2, line1, ) assert lines == geometry.MultiLineString( ( ((0, 0, 0), (1, 1, 3), (2, 2, 6)), ((0, 0), (1, 1), (2, 2)), ((0, 0, 0), (1, 1, 3), (2, 2, 6)), ((0, 0), (1, 1), (2, 2)), ((0, 0, 0), (1, 1, 3), (2, 2, 6)), ), unique=False, ) def test_is_empty() -> None: lines = geometry.MultiLineString([]) assert lines.is_empty def test_repr_empty() -> None: lines = geometry.MultiLineString([]) assert repr(lines) == "MultiLineString(())" def test_empty_bounds() -> None: lines = geometry.MultiLineString([]) assert lines.bounds == () ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_multipoint.py0000644000175100001770000001022314600335465017546 0ustar00runnerdocker"""Test MultiPoint.""" import pytest from pygeoif import geometry def test_geoms() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) for point in multipoint.geoms: assert type(point) is geometry.Point def test_len() -> None: multipoint = geometry.MultiPoint( [(0, 0), (1, 1), (1, 2), (2, 2), (0, 0), (1, 1), (1, 2), (2, 2)], ) assert len(multipoint) == 8 def test_bounds() -> None: multipoint = geometry.MultiPoint( [(0, 1), (1, 1), (3, 2)], ) assert multipoint.bounds == (0, 1, 3, 2) def test_has_z_empty() -> None: multipoint = geometry.MultiPoint(()) assert multipoint.has_z is None def test_geo_interface() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) assert multipoint.__geo_interface__ == { "type": "MultiPoint", "bbox": (0, 0, 2, 2), "coordinates": ((0, 0), (1, 1), (1, 2), (2, 2)), } def test_from_dict() -> None: multipoint = geometry.MultiPoint._from_dict( { "type": "MultiPoint", "bbox": (0.0, 0.0, 1.0, 1.0), # pragma: no mutate "coordinates": ((0.0, 0.0), (1.0, 1.0)), }, ) assert multipoint.__geo_interface__ == { "type": "MultiPoint", "bbox": (0.0, 0.0, 1.0, 1.0), # pragma: no mutate "coordinates": ((0.0, 0.0), (1.0, 1.0)), } def test_coords() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) with pytest.raises( NotImplementedError, match="^Multi-part geometries do not provide a coordinate sequence$", ): assert multipoint.coords def test_unique() -> None: multipoint = geometry.MultiPoint( [(0, 0), (1, 1), (1, 2), (2.0, 2.0), (0, 0), (1.0, 1.0), (1, 2), (2, 2)], unique=True, ) assert len(multipoint) == 4 def test_wkt() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) assert multipoint.wkt == "MULTIPOINT (0 0, 1 1, 1 2, 2 2)" def test_repr() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) assert repr(multipoint) == "MultiPoint(((0, 0), (1, 1), (1, 2), (2, 2)))" def test_repr_eval() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) assert ( eval( repr(multipoint), {}, {"MultiPoint": geometry.MultiPoint}, ).__geo_interface__ == multipoint.__geo_interface__ ) def test_convex_hull() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (2, 2)]) assert multipoint.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d() -> None: multipoint = geometry.MultiPoint([(0, 0, 0), (1, 1, 1), (2, 2, 2)]) assert multipoint.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d_collapsed_to_point() -> None: multipoint = geometry.MultiPoint([(0, 0, 0), (0, 0, 1), (0, 0, 2)]) assert multipoint.convex_hull == geometry.Point(0, 0) def test_convex_hull_linear_ring() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 0), (2, 2)]) assert multipoint.convex_hull == geometry.Polygon([(0, 0), (1, 0), (2, 2), (0, 0)]) def test_from_points() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 0), (2, 2)]) p1 = geometry.Point(0, 0) p2 = geometry.Point(1, 0) p3 = geometry.Point(2.0, 2.0) assert geometry.MultiPoint.from_points(p1, p2, p3) == multipoint def test_from_points_unique() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 0), (2, 2)], unique=True) p1 = geometry.Point(0, 0) p2 = geometry.Point(1, 0) p3 = geometry.Point(2.0, 2.0) assert ( geometry.MultiPoint.from_points(p1, p2, p3, p1, p2, p3, p1, unique=True) == multipoint ) def test_empty() -> None: multipoint = geometry.MultiPoint([(1, None)]) assert multipoint.is_empty def test_repr_empty() -> None: multipoint = geometry.MultiPoint([(None, None)]) assert repr(multipoint) == "MultiPoint(((),))" def test_empty_bounds() -> None: multipoint = geometry.MultiPoint([(None, None)]) assert multipoint.bounds == () ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_multipolygon.py0000644000175100001770000001730714600335465020116 0ustar00runnerdocker"""Test MultiPolygon.""" from pygeoif import geometry def test_geoms() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))], ), ], ) for poly in polys.geoms: assert type(poly) is geometry.Polygon def test_len() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], ), (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),), ], ) assert len(polys) == 2 def test_geo_interface() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], ), ], ) assert polys.__geo_interface__ == { "type": "MultiPolygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": ( ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)), ), ), } def test_from_dict() -> None: polys = geometry.MultiPolygon._from_dict( { "type": "MultiPolygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": ( ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)), ), ), }, ) assert polys.__geo_interface__ == { "type": "MultiPolygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": ( ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)), ), ), } def test_wkt() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], ), (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),), ], ) assert polys.wkt == ( "MULTIPOLYGON (((0.0 0.0, 0.0 1.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)," "(0.1 0.1, 0.1 0.2, 0.2 0.2, 0.2 0.1, 0.1 0.1))," "((0.0 0.0, 0.0 1.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)))" ) def test_repr() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], ), (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),), ], ) assert repr(polys) == ( "MultiPolygon(((((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), " "(((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)),)), " "(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),)))" ) def test_repr_eval() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], ), (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),), ], ) assert ( eval( repr(polys), {}, {"MultiPolygon": geometry.MultiPolygon}, ).__geo_interface__ == polys.__geo_interface__ ) def test_convex_hull() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (0.0, 0.0)), [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], ), (((0.0, 0.0), (0.0, 2.0), (0, 0)),), ], ) assert polys.convex_hull == geometry.LineString([(0.0, 0.0), (0.0, 2.0)]) def test_convex_hull_3d() -> None: polys = geometry.MultiPolygon( [ (((0, 0, 1), (1, 1, 2), (0, 0, 1)),), (((0, 0, 3), (2, 2, 4), (0, 0, 3)),), ], ) assert polys.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d_collapsed_to_point() -> None: polys = geometry.MultiPolygon( [ (((0, 0, 1), (0, 0, 2), (0, 0, 3)),), (((0, 0, 3), (0, 0, 4), (0, 0, 5)),), ], ) assert polys.convex_hull == geometry.Point(0, 0) def test_convex_hull_linear_ring() -> None: polys = geometry.MultiPolygon( [ (((0, 0), (1, 0), (2, 2)),), (((0, 0), (1, 1), (1, 2)),), ], ) assert polys.convex_hull == geometry.Polygon( [(0, 0), (1, 0), (2, 2), (1, 2), (0, 0)], ) def test_unique() -> None: polys = geometry.MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), (((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1)),), ), (((0, 0), (0, 1), (1, 1), (1, 0)),), ( ((0, 0), (0, 1), (1, 1), (1, 0)), (((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1)),), ), (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),), ], unique=True, ) assert len(polys) == 2 def test_from_polygons() -> None: e1 = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i1 = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] i2 = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon1 = geometry.Polygon(e1, [i1, i2]) polygon2 = geometry.Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon3 = geometry.Polygon(e, [i]) polys = geometry.MultiPolygon.from_polygons(polygon1, polygon2, polygon3) assert polys == geometry.MultiPolygon( ( ( ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), ( ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), ), ), (((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),), ( ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), (((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),), ), ), ) def test_from_polygons_unique() -> None: e1 = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i1 = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] i2 = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon1 = geometry.Polygon(e1, [i1, i2]) polygon2 = geometry.Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)]) e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon3 = geometry.Polygon(e, [i]) polys = geometry.MultiPolygon.from_polygons( polygon1, polygon2, polygon3, unique=True, ) polys2 = geometry.MultiPolygon.from_polygons( polygon1, polygon2, polygon3, polygon1, polygon2, polygon3, unique=True, ) assert polys == polys2 def test_is_empty() -> None: polys = geometry.MultiPolygon([]) assert polys.is_empty def test_empty_wkt() -> None: polys = geometry.MultiPolygon([]) assert polys.wkt == "MULTIPOLYGON EMPTY" def test_repr_empty() -> None: polys = geometry.MultiPolygon([]) assert repr(polys) == "MultiPolygon(())" def test_empty_bounds() -> None: polys = geometry.MultiPolygon([]) assert polys.bounds == () ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_point.py0000644000175100001770000001163514600335465016503 0ustar00runnerdocker"""Test Point.""" import math from unittest import mock import pytest from pygeoif import geometry from pygeoif.exceptions import DimensionError def test_empty() -> None: point = geometry.Point(1, None) assert point.is_empty def test_empty_nan() -> None: point = geometry.Point(1, math.nan, math.nan) assert point.is_empty def test_bool() -> None: point = geometry.Point(1.0, 0.0) assert point def test_bool_empty() -> None: point = geometry.Point(None, None) assert not point def test_empty_wkt() -> None: point = geometry.Point(None, None) assert point.wkt == "POINT EMPTY" def test_bounds() -> None: point = geometry.Point(1.0, 0.0) assert point.bounds == (1.0, 0, 1, 0) def test_bounds3d() -> None: point = geometry.Point(1.0, 0.0, 3.0) # pragma: no mutate assert point.bounds == (1, 0, 1, 0) def test_xy() -> None: point = geometry.Point(1.0, 0.0) assert point.x == 1 assert point.y == 0 def test_xy_raises_error_accessing_z() -> None: point = geometry.Point(1, 0) with pytest.raises( DimensionError, match=r"^The Point\(1, 0\) geometry does not have z values$", ): assert point.z def test_xyz() -> None: point = geometry.Point(1.0, 0.0, 2.0) assert point.x == 1 assert point.y == 0 assert point.z == 2 def test_repr_empty() -> None: point = geometry.Point(None, None) assert repr(point) == "Point()" def test_repr2d() -> None: point = geometry.Point(1, 0) assert repr(point) == "Point(1, 0)" assert not point.has_z def test_repr3d() -> None: point = geometry.Point(1.0, 2.0, 3.0) assert repr(point) == "Point(1.0, 2.0, 3.0)" assert point.has_z def test_has_z_2d() -> None: point = geometry.Point(1, 0) assert not point.has_z def test_has_z_3d() -> None: point = geometry.Point(1.0, 2.0, 3.0) assert point.has_z def test_repr_eval() -> None: point = geometry.Point(1.0, 2.0, 3.0) assert eval(repr(point), {}, {"Point": geometry.Point}) == point def test_wkt2d() -> None: point = geometry.Point(1, 0) assert str(point) == "POINT (1 0)" def test_wkt3d() -> None: point = geometry.Point(1.0, 0.0, 3.0) assert str(point) == "POINT Z (1.0 0.0 3.0)" def test_coords_get() -> None: point = geometry.Point(1.0, 0.0, 3.0) assert point.coords == ((1, 0, 3),) def test_geo_interface() -> None: point = geometry.Point(0, 1, 2) assert point.__geo_interface__ == { "type": "Point", "bbox": (0, 1, 0, 1), # pragma: no mutate "coordinates": (0.0, 1.0, 2.0), } def test_geo_interface_empty() -> None: point = geometry.Point(None, None) with pytest.raises(AttributeError, match="^Empty Geometry$"): assert point.__geo_interface__ def test_from_dict() -> None: point = geometry.Point._from_dict({"type": "Point", "coordinates": (0.0, 1.0, 2.0)}) assert point.coords == ((0, 1, 2),) def test_from_dict_wrong_type() -> None: with pytest.raises(ValueError, match="^You cannot assign Xoint to Point"): geometry.Point._from_dict( {"type": "Xoint", "coordinates": (0.0, 1.0, 2.0)}, # pragma: no mutate ) def test_from_compatible() -> None: not_a_geometry = mock.Mock() not_a_geometry.__geo_interface__ = { "type": "Point", "bbox": (0, 1, 0, 1), # pragma: no mutate "coordinates": (0.0, 1.0, 2.0), } point = geometry.Point._from_interface(not_a_geometry) assert isinstance(point, geometry.Point) assert point.coords == ((0, 1, 2),) def test_eq_interface() -> None: not_a_geometry = mock.Mock() not_a_geometry.__geo_interface__ = { "type": "Point", "coordinates": (0.0, 1.0, 2.0), } point = geometry.Point(0, 1, 2) assert point == not_a_geometry def test_eq_floats() -> None: point1 = geometry.Point(0.3, 0.6) point2 = geometry.Point(0.2 + 0.1, 0.3 * 2) assert point1 == point2 def test_neq_missing_interface() -> None: point = geometry.Point(0, 1, 2) assert point != object() def test_neq_interface_coords() -> None: not_a_geometry = mock.Mock() not_a_geometry.__geo_interface__ = { "type": "Point", "coordinates": (0.0, 1.0, 2.0), } point = geometry.Point(0, 0, 2) assert point != not_a_geometry def test_convex_hull() -> None: point = geometry.Point(1, 0) assert point.convex_hull == point def test_convex_hull_3d() -> None: point = geometry.Point(1, 2, 3) assert point.convex_hull == geometry.Point(1, 2) def test_from_coordinates() -> None: point = geometry.Point(1, 2) assert geometry.Point.from_coordinates(point.coords) == point def test_from_coordinates_3d() -> None: point = geometry.Point(1, 2, 3) assert geometry.Point.from_coordinates(point.coords) == point def test_empty_bounds() -> None: point = geometry.Point(None, None) assert point.bounds == () ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1711389493.0 pygeoif-1.4.0/tests/test_polygon.py0000644000175100001770000001730414600335465017040 0ustar00runnerdocker"""Test Polygon.""" from unittest import mock from pygeoif import geometry def test_coords() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) assert polygon.coords == (((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),) def test_coords_with_holes() -> None: e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon = geometry.Polygon(e, [i]) assert polygon.coords == ( ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)), (((1.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (1.0, 0.0)),), ) def test_geo_interface_shell_only() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) assert polygon.__geo_interface__, { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": (((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),), } def test_geo_interface_with_holes() -> None: e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon = geometry.Polygon(e, [i]) assert polygon.__geo_interface__ == { "type": "Polygon", "bbox": (0.0, 0.0, 2.0, 2.0), "coordinates": ( ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)), ((1.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (1.0, 0.0)), ), } def test_from_dict_shell_only() -> None: polygon = geometry.Polygon._from_dict( { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": (((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),), }, ) assert polygon.__geo_interface__, { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), "coordinates": (((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),), } def test_from_dict_with_holes() -> None: polygon = geometry.Polygon._from_dict( { "type": "Polygon", "bbox": (0.0, 0.0, 2.0, 2.0), "coordinates": ( ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)), ((1.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (1.0, 0.0)), ), }, ) assert polygon.__geo_interface__ == { "type": "Polygon", "bbox": (0.0, 0.0, 2.0, 2.0), "coordinates": ( ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)), ((1.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (1.0, 0.0)), ), } def test_from_compatible() -> None: not_a_geometry = mock.Mock( __geo_interface__={ "type": "Polygon", "bbox": (0.0, 0.0, 2.0, 2.0), "coordinates": ( ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)), ((1.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (1.0, 0.0)), ), }, ) polygon = geometry.Polygon._from_interface(not_a_geometry) assert polygon.__geo_interface__ == { "type": "Polygon", "bbox": (0.0, 0.0, 2.0, 2.0), "coordinates": ( ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)), ((1.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (1.0, 0.0)), ), } def test_exteriors() -> None: polygon = geometry.Polygon([(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)]) assert polygon.exterior.coords == ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)) def test_interiors() -> None: e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon = geometry.Polygon(e, [i]) assert next(iter(polygon.interiors)).coords == ( (1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0), ) def test_bounds() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) assert polygon.bounds == (0.0, 0.0, 1.0, 1.0) def test_wkt_shell_only() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)], []) assert polygon.wkt == "POLYGON ((0 0, 1 1, 1 0, 0 0))" def test_wkt_with_holes() -> None: e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] i2 = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon = geometry.Polygon(e, [i, i2]) assert polygon.wkt == ( "POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0)," "(1 0, 0.5 0.5, 1 1, 1.5 0.5, 1 0)," "(1 0, 0.5 0.5, 1 1, 1.5 0.5, 1 0))" ) def test_wkt_shell_only_3d() -> None: polygon = geometry.Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)]) assert polygon.wkt == "POLYGON Z ((0 0 0, 1 1 0, 1 0 0, 0 0 0))" def test_repr() -> None: e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon = geometry.Polygon(e, [i]) assert repr(polygon) == ( "Polygon(((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), " "(((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),))" ) def test_repr_shell_only() -> None: polygon = geometry.Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)]) assert repr(polygon) == "Polygon(((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),)" def test_repr_eval() -> None: e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon = geometry.Polygon(e, [i]) assert ( eval(repr(polygon), {}, {"Polygon": geometry.Polygon}).__geo_interface__ == polygon.__geo_interface__ ) def test_repr_eval_shell_only() -> None: polygon = geometry.Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)]) assert ( eval(repr(polygon), {}, {"Polygon": geometry.Polygon}).__geo_interface__ == polygon.__geo_interface__ ) def test_hasz() -> None: polygon = geometry.Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)]) assert polygon.has_z def test_convex_hull() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (2, 2)]) assert polygon.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d() -> None: polygon = geometry.Polygon([(0, 0, 0), (1, 1, 1), (2, 2, 2)]) assert polygon.convex_hull == geometry.LineString([(0, 0), (2, 2)]) def test_convex_hull_3d_collapsed_to_point() -> None: polygon = geometry.Polygon([(0, 0, 0), (0, 0, 1), (0, 0, 2)]) assert polygon.convex_hull == geometry.Point(0, 0) def test_convex_hull_linear_ring() -> None: polygon = geometry.Polygon([(0, 0), (1, 0), (2, 2)]) assert polygon.convex_hull == geometry.Polygon([(0, 0), (1, 0), (2, 2), (0, 0)]) def test_from_linear_rings() -> None: ring1 = geometry.LinearRing([(0, 0), (1, 1), (2, 2)]) ring2 = geometry.LinearRing(((0, 0), (1, 1), (1, 0), (0, 0))) assert geometry.Polygon.from_linear_rings(ring1, ring2) == geometry.Polygon( ((0, 0), (1, 1), (2, 2), (0, 0)), (((0, 0), (1, 1), (1, 0), (0, 0)),), ) def test_from_coordinates() -> None: polygon = geometry.Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)]) assert geometry.Polygon.from_coordinates(polygon.coords) == polygon def test_from_coordinates_with_holes() -> None: e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] polygon = geometry.Polygon(e, [i]) assert geometry.Polygon.from_coordinates(polygon.coords) == polygon def test_empty() -> None: polygon = geometry.Polygon([]) assert polygon.is_empty def test_empty_wkt() -> None: polygon = geometry.Polygon([]) assert polygon.wkt == "POLYGON EMPTY" def test_repr_empty() -> None: polygon = geometry.Polygon([]) assert repr(polygon) == "Polygon((),)" def test_empty_bounds() -> None: polygon = geometry.Polygon([]) assert polygon.bounds == ()