pax_global_header00006660000000000000000000000064145362662760014533gustar00rootroot0000000000000052 comment=2d277acfa4b44eaad4f0675469fc323b27921d1f geomet-1.1.0/000077500000000000000000000000001453626627600130125ustar00rootroot00000000000000geomet-1.1.0/AUTHORS.txt000066400000000000000000000010201453626627600146710ustar00rootroot00000000000000# Authors of GeoMet # Name - Month Year Lars Butler - March 2013 Maralla - November 2013 Sean Gillies - August 2014 Tom Caruso - December 2018 Paul Bryan - August 2019 Ram Rachum - June 2020 Andrew Chapkowski - June 2020 Vadim Kozyrevskii - January 2021 Stian Jensen - January 2022 Mike Taves - May 2023 geomet-1.1.0/LICENSE000066400000000000000000000260751453626627600140310ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. geomet-1.1.0/PKG-INFO000066400000000000000000000267001453626627600141140ustar00rootroot00000000000000Metadata-Version: 2.1 Name: geomet Version: 1.1.0 Summary: Pure Python conversion library for common geospatial data formats Maintainer-email: Lars Butler License: Apache-2.0 Project-URL: Repository, https://github.com/geomet/geomet Keywords: esri,ewkb,ewkt,geojson,geopackage,geospatial,gis,spatial,wkb,wkt Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.7 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: Topic :: Scientific/Engineering :: GIS Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE License-File: AUTHORS.txt # GeoMet [![geomet](https://circleci.com/gh/geomet/geomet.svg?style=shield)](https://app.circleci.com/pipelines/github/geomet) Pure-Python conversion library for common geospatial data formats. Supported formats include: - [GeoJSON](http://www.geojson.org/geojson-spec.html) - [WKT/WKB](http://en.wikipedia.org/wiki/Well-known_text) (Well-Known Text/Binary) - [Extended WKB/WKT](https://postgis.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT) - [GeoPackage Binary](http://www.geopackage.org/spec/#gpb_format) ## Install Install the latest version from [PyPI](https://pypi.org/project/geomet/): $ pip install geomet ## Functionality Converion functions are exposed through idiomatic `load/loads/dump/dumps` interfaces. GeoMet is intended to cover all common use cases for dealing with 2D, 3D, and 4D geometries (including 'Z', 'M', and 'ZM'). | Geometry | WKT/EWKT | WKB/EWKB | GeoPackage Binary | EsriJSON | | -------- | :------: | :------: | :---------------: | :------: | | Point | ✅ | ✅ | ✅| ✅ | | LineString | ✅ | ✅ | ✅| ✅ | | Polygon | ✅ | ✅ | ✅| ✅ | | MultiPoint | ✅ | ✅ | ✅| ✅ | | MultiLineString | ✅ | ✅ | ✅| ✅ | | MultiPolygon | ✅ | ✅ | ✅| ✅ | | GeometryCollection | ✅ | ✅ | ✅| ✅ | ## Example usage Coverting a 'Point' GeoJSON object to WKT: >>> from geomet import wkt >>> point = {'type': 'Point', 'coordinates': [116.4, 45.2, 11.1]} >>> wkt.dumps(point, decimals=4) 'POINT (116.4000 45.2000 11.1000)' Converting a 'Point' GeoJSON object to WKB: >>> from geomet import wkb >>> wkb.dumps(point) b'\x00\x00\x00\x10\x01@]\x19\x99\x99\x99\x99\x9a@F\x99\x99\x99\x99\x99\x9a@&333333' >>> wkb.dumps(point, big_endian=False) b'\x01\x01\x10\x00\x00\x9a\x99\x99\x99\x99\x19]@\x9a\x99\x99\x99\x99\x99F@333333&@' Converting a 'Point' GeoJSON object to GeoPackage Binary: >>> from geomet import geopackage >>> geopackage.dumps(point) b'GP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xe9@]\x19\x99\x99\x99\x99\x9a@F\x99\x99\x99\x99\x99\x9a@&333333' >>> geopackage.dumps(point, big_endian=False) b'GP\x00\x01\x00\x00\x00\x00\x01\xe9\x03\x00\x00\x9a\x99\x99\x99\x99\x19]@\x9a\x99\x99\x99\x99\x99F@333333&@' Converting a 'LineString' GeoJSON object to WKT: >>> linestring = {'type':'LineString', ... 'coordinates': [[0.0, 0.0, 10.0], [2.0, 1.0, 20.0], ... [4.0, 2.0, 30.0], [5.0, 4.0, 40.0]]} >>> wkt.dumps(linestring, decimals=0) 'LINESTRING (0 0 10, 2 1 20, 4 2 30, 5 4 40)' Converting a 'LineString' GeoJSON object to WKB: >>> wkb.dumps(linestring) b'\x00\x00\x00\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xf0\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@>\x00\x00\x00\x00\x00\x00@\x14\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@D\x00\x00\x00\x00\x00\x00' >>> wkb.dumps(linestring, big_endian=False) b'\x01\x02\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00>@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00D@' Converting a 'LineString' GeoJSON object to GeoPackage Binary: >>> geopackage.dumps(linestring) b'GP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xf0\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@>\x00\x00\x00\x00\x00\x00@\x14\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@D\x00\x00\x00\x00\x00\x00' >>> geopackage.dumps(linestring, big_endian=False) b'GP\x00\x01\x00\x00\x00\x00\x01\x02\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00>@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00D@' Converting 'Point' WKT to GeoJSON: >>> wkt.loads('POINT(10 20)') {'type': 'Point', 'coordinates': [10.0, 20.0]} Coverting 'GeometryCollection' WKT to GeoJSON: >>> wkt.loads('GEOMETRYCOLLECTION(POINT(10 20),POLYGON(((0 0), (10 30), (30 10), (0 0)))') {'type': 'GeometryCollection', 'geometries': [{'type': 'Point', 'coordinates': [10.0, 20.0]}, {'type': 'Polygon', 'coordinates': [[[0.0, 0.0]], [[10.0, 30.0]], [[30.0, 10.0]], [[0.0, 0.0]]]}]} [EWKT/EWKB](http://postgis.net/documentation/manual-2.1/using_postgis_dbmanagement.html#EWKB_EWKT) are also supported for all geometry types. This uses a custom extension to the GeoJSON standard in order to preserve SRID information through conversions. For example: >>> wkt.loads('SRID=4326;POINT(10 20)') {'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}} >>> wkt.dumps({'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}, 'crs': {'properties': {'name': 'EPSG4326'}, 'type': 'name'}}) 'SRID=4326;POINT (10.0000000000000000 20.0000000000000000)' >>> wkb.loads('\x00 \x00\x00\x01\x00\x00\x10\xe6@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00') {'meta': {'srid': '4326'}, 'type': 'Point', 'coordinates': [10.0, 20.0]} >>> wkb.dumps({'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}, 'crs': {'properties': {'name': 'EPSG4326'}, 'type': 'name'}}) '\x00 \x00\x00\x01\x00\x00\x10\xe6@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00' GeoPackage binary supports encoding of SRID and envelope information. If your geopackage has an envelope specified, then it will be added into the resulting GeoJSON in a key called `'bbox'`: >>> gpkg = b'GP\x00\x03\x00\x00\x00\x00\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@\x01\x01\x00\x00\x00\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@' >>> geopackage.loads(gpkg) >>> {'type': 'Point', 'coordinates': [9.615277517659223, 38.55870291467437], 'bbox': (9.615277517659223, 38.55870291467437, 9.615277517659223, 38.55870291467437)} In the same way, if a 'bbox' key is present on a `dumps`-ed geometry, it will be added to the header of the GeoPackage geometry: >>> polygon = {'type': 'Polygon', 'coordinates': [[[20.0, 20.0], [34.0, 124.0], [70.0, 140.0], [130.0, 130.0], [70.0, 100.0], [110.0, 70.0], [170.0, 20.0], [90.0, 10.0], [20.0, 20.0]]], 'bbox': (20.0, 170.0, 10.0, 140.0)} >>> geopackage.dumps(polygon) b'GP\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00@e@\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x80a@\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\t@4\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@A\x00\x00\x00\x00\x00\x00@_\x00\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@a\x80\x00\x00\x00\x00\x00@`@\x00\x00\x00\x00\x00@`@\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@Y\x00\x00\x00\x00\x00\x00@[\x80\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@e@\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@V\x80\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00' If an integer SRID identifier is present in a `'meta'` key (like `'meta': {'srid': 4326}`), then the SRID will be included in the GeoPackage header. ## History This library was originally created as the result of a bug report related to another project: https://bugs.launchpad.net/openquake-old/+bug/1073909. The source of this issue was largely due to a dependency on [GEOS](https://libgeos.org/), which is written in C/C++. Depending on GEOS requires any data conversion bug fixes to happen upstream, which takes time and effort. Ultimately, this was the inspiration to create a more lightweight, pure-Python conversion library as an alterntive tool for reliably converting data between various geospatial formats. The name "GeoMet" was inspired by "met", the German word for [mead](http://en.wikipedia.org/wiki/Mead). It is also a shortened version of the word "geometry". ## Limitations ### Outputing "empty" geometries to binary formats is not supported Attempting to output an empty geometry to a binary format will result in an exception: `ValueError: Empty geometries cannot be represented in WKB. Reason: The dimensionality of the WKB would be ambiguous.` There are a few reasons for this this limitation: - Any `EMTPY` geometry (e.g., `POINT EMPTY`, `MULTIPOLYGON EMPTY`, etc.) cannot be converted into binary format because binary formats such as WKB require an explicit dimension type (2d, Z, M, or ZM). This means that some objects cannot be reliably converted to and from different formats in a [bijective](https://en.wikipedia.org/wiki/Bijection) manner. - The [GeoJSON standard](https://www.rfc-editor.org/rfc/rfc7946) does have a way of representing empty geometries; however, details are minimal and the dimensionality of such an object remains ambiguous. - Representing some geometry types (such as points and lines) as "empty" is [deeply flawed to begin with](http://aleph0.clarku.edu/~djoyce/elements/bookI/defI1.html). For example, a point can represent any location in 2d, 3d, or 4d space. However, a point is infinitesimally small (it has no size) and it can't contain anything (it can't be "full"), therefore, it doesn't make sense for a point to be "empty". As a result, GeoMet has chosen to not attempt to address these problems, and simply raise an exception instead. Example: >>> import geomet >>> import geomet.wkt as wkt >>> import geomet.wkb as wkb >>> pt = wkt.loads('POINT EMPTY') >>> pt {'type': 'Point', 'coordinates': []} >>> wkb.dumps(pt) Traceback (most recent call last): File "", line 1, in File "/home/jdoe/geomet/geomet/wkb.py", line 216, in dumps return _dumps(obj, big_endian) File "/home/jdoe/geomet/geomet/wkb.py", line 238, in _dumps raise ValueError( ValueError: Empty geometries cannot be represented in WKB. Reason: The dimensionality of the WKB would be ambiguous. ## See also - [wellknown](https://github.com/mapbox/wellknown): A similar package for Node.js. - [geo](https://github.com/bryanjos/geo): A nearly-identical package for Elixir. geomet-1.1.0/README.md000066400000000000000000000245631453626627600143030ustar00rootroot00000000000000# GeoMet [![geomet](https://circleci.com/gh/geomet/geomet.svg?style=shield)](https://app.circleci.com/pipelines/github/geomet) Pure-Python conversion library for common geospatial data formats. Supported formats include: - [GeoJSON](http://www.geojson.org/geojson-spec.html) - [WKT/WKB](http://en.wikipedia.org/wiki/Well-known_text) (Well-Known Text/Binary) - [Extended WKB/WKT](https://postgis.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT) - [GeoPackage Binary](http://www.geopackage.org/spec/#gpb_format) ## Install Install the latest version from [PyPI](https://pypi.org/project/geomet/): $ pip install geomet ## Functionality Converion functions are exposed through idiomatic `load/loads/dump/dumps` interfaces. GeoMet is intended to cover all common use cases for dealing with 2D, 3D, and 4D geometries (including 'Z', 'M', and 'ZM'). | Geometry | WKT/EWKT | WKB/EWKB | GeoPackage Binary | EsriJSON | | -------- | :------: | :------: | :---------------: | :------: | | Point | ✅ | ✅ | ✅| ✅ | | LineString | ✅ | ✅ | ✅| ✅ | | Polygon | ✅ | ✅ | ✅| ✅ | | MultiPoint | ✅ | ✅ | ✅| ✅ | | MultiLineString | ✅ | ✅ | ✅| ✅ | | MultiPolygon | ✅ | ✅ | ✅| ✅ | | GeometryCollection | ✅ | ✅ | ✅| ✅ | ## Example usage Coverting a 'Point' GeoJSON object to WKT: >>> from geomet import wkt >>> point = {'type': 'Point', 'coordinates': [116.4, 45.2, 11.1]} >>> wkt.dumps(point, decimals=4) 'POINT (116.4000 45.2000 11.1000)' Converting a 'Point' GeoJSON object to WKB: >>> from geomet import wkb >>> wkb.dumps(point) b'\x00\x00\x00\x10\x01@]\x19\x99\x99\x99\x99\x9a@F\x99\x99\x99\x99\x99\x9a@&333333' >>> wkb.dumps(point, big_endian=False) b'\x01\x01\x10\x00\x00\x9a\x99\x99\x99\x99\x19]@\x9a\x99\x99\x99\x99\x99F@333333&@' Converting a 'Point' GeoJSON object to GeoPackage Binary: >>> from geomet import geopackage >>> geopackage.dumps(point) b'GP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xe9@]\x19\x99\x99\x99\x99\x9a@F\x99\x99\x99\x99\x99\x9a@&333333' >>> geopackage.dumps(point, big_endian=False) b'GP\x00\x01\x00\x00\x00\x00\x01\xe9\x03\x00\x00\x9a\x99\x99\x99\x99\x19]@\x9a\x99\x99\x99\x99\x99F@333333&@' Converting a 'LineString' GeoJSON object to WKT: >>> linestring = {'type':'LineString', ... 'coordinates': [[0.0, 0.0, 10.0], [2.0, 1.0, 20.0], ... [4.0, 2.0, 30.0], [5.0, 4.0, 40.0]]} >>> wkt.dumps(linestring, decimals=0) 'LINESTRING (0 0 10, 2 1 20, 4 2 30, 5 4 40)' Converting a 'LineString' GeoJSON object to WKB: >>> wkb.dumps(linestring) b'\x00\x00\x00\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xf0\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@>\x00\x00\x00\x00\x00\x00@\x14\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@D\x00\x00\x00\x00\x00\x00' >>> wkb.dumps(linestring, big_endian=False) b'\x01\x02\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00>@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00D@' Converting a 'LineString' GeoJSON object to GeoPackage Binary: >>> geopackage.dumps(linestring) b'GP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xf0\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@>\x00\x00\x00\x00\x00\x00@\x14\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@D\x00\x00\x00\x00\x00\x00' >>> geopackage.dumps(linestring, big_endian=False) b'GP\x00\x01\x00\x00\x00\x00\x01\x02\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00>@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00D@' Converting 'Point' WKT to GeoJSON: >>> wkt.loads('POINT(10 20)') {'type': 'Point', 'coordinates': [10.0, 20.0]} Coverting 'GeometryCollection' WKT to GeoJSON: >>> wkt.loads('GEOMETRYCOLLECTION(POINT(10 20),POLYGON(((0 0), (10 30), (30 10), (0 0)))') {'type': 'GeometryCollection', 'geometries': [{'type': 'Point', 'coordinates': [10.0, 20.0]}, {'type': 'Polygon', 'coordinates': [[[0.0, 0.0]], [[10.0, 30.0]], [[30.0, 10.0]], [[0.0, 0.0]]]}]} [EWKT/EWKB](http://postgis.net/documentation/manual-2.1/using_postgis_dbmanagement.html#EWKB_EWKT) are also supported for all geometry types. This uses a custom extension to the GeoJSON standard in order to preserve SRID information through conversions. For example: >>> wkt.loads('SRID=4326;POINT(10 20)') {'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}} >>> wkt.dumps({'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}, 'crs': {'properties': {'name': 'EPSG4326'}, 'type': 'name'}}) 'SRID=4326;POINT (10.0000000000000000 20.0000000000000000)' >>> wkb.loads('\x00 \x00\x00\x01\x00\x00\x10\xe6@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00') {'meta': {'srid': '4326'}, 'type': 'Point', 'coordinates': [10.0, 20.0]} >>> wkb.dumps({'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}, 'crs': {'properties': {'name': 'EPSG4326'}, 'type': 'name'}}) '\x00 \x00\x00\x01\x00\x00\x10\xe6@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00' GeoPackage binary supports encoding of SRID and envelope information. If your geopackage has an envelope specified, then it will be added into the resulting GeoJSON in a key called `'bbox'`: >>> gpkg = b'GP\x00\x03\x00\x00\x00\x00\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@\x01\x01\x00\x00\x00\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@' >>> geopackage.loads(gpkg) >>> {'type': 'Point', 'coordinates': [9.615277517659223, 38.55870291467437], 'bbox': (9.615277517659223, 38.55870291467437, 9.615277517659223, 38.55870291467437)} In the same way, if a 'bbox' key is present on a `dumps`-ed geometry, it will be added to the header of the GeoPackage geometry: >>> polygon = {'type': 'Polygon', 'coordinates': [[[20.0, 20.0], [34.0, 124.0], [70.0, 140.0], [130.0, 130.0], [70.0, 100.0], [110.0, 70.0], [170.0, 20.0], [90.0, 10.0], [20.0, 20.0]]], 'bbox': (20.0, 170.0, 10.0, 140.0)} >>> geopackage.dumps(polygon) b'GP\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00@e@\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x80a@\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\t@4\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@A\x00\x00\x00\x00\x00\x00@_\x00\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@a\x80\x00\x00\x00\x00\x00@`@\x00\x00\x00\x00\x00@`@\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@Y\x00\x00\x00\x00\x00\x00@[\x80\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@e@\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@V\x80\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00' If an integer SRID identifier is present in a `'meta'` key (like `'meta': {'srid': 4326}`), then the SRID will be included in the GeoPackage header. ## History This library was originally created as the result of a bug report related to another project: https://bugs.launchpad.net/openquake-old/+bug/1073909. The source of this issue was largely due to a dependency on [GEOS](https://libgeos.org/), which is written in C/C++. Depending on GEOS requires any data conversion bug fixes to happen upstream, which takes time and effort. Ultimately, this was the inspiration to create a more lightweight, pure-Python conversion library as an alterntive tool for reliably converting data between various geospatial formats. The name "GeoMet" was inspired by "met", the German word for [mead](http://en.wikipedia.org/wiki/Mead). It is also a shortened version of the word "geometry". ## Limitations ### Outputing "empty" geometries to binary formats is not supported Attempting to output an empty geometry to a binary format will result in an exception: `ValueError: Empty geometries cannot be represented in WKB. Reason: The dimensionality of the WKB would be ambiguous.` There are a few reasons for this this limitation: - Any `EMTPY` geometry (e.g., `POINT EMPTY`, `MULTIPOLYGON EMPTY`, etc.) cannot be converted into binary format because binary formats such as WKB require an explicit dimension type (2d, Z, M, or ZM). This means that some objects cannot be reliably converted to and from different formats in a [bijective](https://en.wikipedia.org/wiki/Bijection) manner. - The [GeoJSON standard](https://www.rfc-editor.org/rfc/rfc7946) does have a way of representing empty geometries; however, details are minimal and the dimensionality of such an object remains ambiguous. - Representing some geometry types (such as points and lines) as "empty" is [deeply flawed to begin with](http://aleph0.clarku.edu/~djoyce/elements/bookI/defI1.html). For example, a point can represent any location in 2d, 3d, or 4d space. However, a point is infinitesimally small (it has no size) and it can't contain anything (it can't be "full"), therefore, it doesn't make sense for a point to be "empty". As a result, GeoMet has chosen to not attempt to address these problems, and simply raise an exception instead. Example: >>> import geomet >>> import geomet.wkt as wkt >>> import geomet.wkb as wkb >>> pt = wkt.loads('POINT EMPTY') >>> pt {'type': 'Point', 'coordinates': []} >>> wkb.dumps(pt) Traceback (most recent call last): File "", line 1, in File "/home/jdoe/geomet/geomet/wkb.py", line 216, in dumps return _dumps(obj, big_endian) File "/home/jdoe/geomet/geomet/wkb.py", line 238, in _dumps raise ValueError( ValueError: Empty geometries cannot be represented in WKB. Reason: The dimensionality of the WKB would be ambiguous. ## See also - [wellknown](https://github.com/mapbox/wellknown): A similar package for Node.js. - [geo](https://github.com/bryanjos/geo): A nearly-identical package for Elixir. geomet-1.1.0/geomet.egg-info/000077500000000000000000000000001453626627600157645ustar00rootroot00000000000000geomet-1.1.0/geomet.egg-info/PKG-INFO000066400000000000000000000267001453626627600170660ustar00rootroot00000000000000Metadata-Version: 2.1 Name: geomet Version: 1.1.0 Summary: Pure Python conversion library for common geospatial data formats Maintainer-email: Lars Butler License: Apache-2.0 Project-URL: Repository, https://github.com/geomet/geomet Keywords: esri,ewkb,ewkt,geojson,geopackage,geospatial,gis,spatial,wkb,wkt Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.7 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: Topic :: Scientific/Engineering :: GIS Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE License-File: AUTHORS.txt # GeoMet [![geomet](https://circleci.com/gh/geomet/geomet.svg?style=shield)](https://app.circleci.com/pipelines/github/geomet) Pure-Python conversion library for common geospatial data formats. Supported formats include: - [GeoJSON](http://www.geojson.org/geojson-spec.html) - [WKT/WKB](http://en.wikipedia.org/wiki/Well-known_text) (Well-Known Text/Binary) - [Extended WKB/WKT](https://postgis.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT) - [GeoPackage Binary](http://www.geopackage.org/spec/#gpb_format) ## Install Install the latest version from [PyPI](https://pypi.org/project/geomet/): $ pip install geomet ## Functionality Converion functions are exposed through idiomatic `load/loads/dump/dumps` interfaces. GeoMet is intended to cover all common use cases for dealing with 2D, 3D, and 4D geometries (including 'Z', 'M', and 'ZM'). | Geometry | WKT/EWKT | WKB/EWKB | GeoPackage Binary | EsriJSON | | -------- | :------: | :------: | :---------------: | :------: | | Point | ✅ | ✅ | ✅| ✅ | | LineString | ✅ | ✅ | ✅| ✅ | | Polygon | ✅ | ✅ | ✅| ✅ | | MultiPoint | ✅ | ✅ | ✅| ✅ | | MultiLineString | ✅ | ✅ | ✅| ✅ | | MultiPolygon | ✅ | ✅ | ✅| ✅ | | GeometryCollection | ✅ | ✅ | ✅| ✅ | ## Example usage Coverting a 'Point' GeoJSON object to WKT: >>> from geomet import wkt >>> point = {'type': 'Point', 'coordinates': [116.4, 45.2, 11.1]} >>> wkt.dumps(point, decimals=4) 'POINT (116.4000 45.2000 11.1000)' Converting a 'Point' GeoJSON object to WKB: >>> from geomet import wkb >>> wkb.dumps(point) b'\x00\x00\x00\x10\x01@]\x19\x99\x99\x99\x99\x9a@F\x99\x99\x99\x99\x99\x9a@&333333' >>> wkb.dumps(point, big_endian=False) b'\x01\x01\x10\x00\x00\x9a\x99\x99\x99\x99\x19]@\x9a\x99\x99\x99\x99\x99F@333333&@' Converting a 'Point' GeoJSON object to GeoPackage Binary: >>> from geomet import geopackage >>> geopackage.dumps(point) b'GP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xe9@]\x19\x99\x99\x99\x99\x9a@F\x99\x99\x99\x99\x99\x9a@&333333' >>> geopackage.dumps(point, big_endian=False) b'GP\x00\x01\x00\x00\x00\x00\x01\xe9\x03\x00\x00\x9a\x99\x99\x99\x99\x19]@\x9a\x99\x99\x99\x99\x99F@333333&@' Converting a 'LineString' GeoJSON object to WKT: >>> linestring = {'type':'LineString', ... 'coordinates': [[0.0, 0.0, 10.0], [2.0, 1.0, 20.0], ... [4.0, 2.0, 30.0], [5.0, 4.0, 40.0]]} >>> wkt.dumps(linestring, decimals=0) 'LINESTRING (0 0 10, 2 1 20, 4 2 30, 5 4 40)' Converting a 'LineString' GeoJSON object to WKB: >>> wkb.dumps(linestring) b'\x00\x00\x00\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xf0\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@>\x00\x00\x00\x00\x00\x00@\x14\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@D\x00\x00\x00\x00\x00\x00' >>> wkb.dumps(linestring, big_endian=False) b'\x01\x02\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00>@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00D@' Converting a 'LineString' GeoJSON object to GeoPackage Binary: >>> geopackage.dumps(linestring) b'GP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xf0\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@>\x00\x00\x00\x00\x00\x00@\x14\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@D\x00\x00\x00\x00\x00\x00' >>> geopackage.dumps(linestring, big_endian=False) b'GP\x00\x01\x00\x00\x00\x00\x01\x02\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00>@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00D@' Converting 'Point' WKT to GeoJSON: >>> wkt.loads('POINT(10 20)') {'type': 'Point', 'coordinates': [10.0, 20.0]} Coverting 'GeometryCollection' WKT to GeoJSON: >>> wkt.loads('GEOMETRYCOLLECTION(POINT(10 20),POLYGON(((0 0), (10 30), (30 10), (0 0)))') {'type': 'GeometryCollection', 'geometries': [{'type': 'Point', 'coordinates': [10.0, 20.0]}, {'type': 'Polygon', 'coordinates': [[[0.0, 0.0]], [[10.0, 30.0]], [[30.0, 10.0]], [[0.0, 0.0]]]}]} [EWKT/EWKB](http://postgis.net/documentation/manual-2.1/using_postgis_dbmanagement.html#EWKB_EWKT) are also supported for all geometry types. This uses a custom extension to the GeoJSON standard in order to preserve SRID information through conversions. For example: >>> wkt.loads('SRID=4326;POINT(10 20)') {'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}} >>> wkt.dumps({'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}, 'crs': {'properties': {'name': 'EPSG4326'}, 'type': 'name'}}) 'SRID=4326;POINT (10.0000000000000000 20.0000000000000000)' >>> wkb.loads('\x00 \x00\x00\x01\x00\x00\x10\xe6@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00') {'meta': {'srid': '4326'}, 'type': 'Point', 'coordinates': [10.0, 20.0]} >>> wkb.dumps({'type': 'Point', 'coordinates': [10.0, 20.0], 'meta': {'srid': '4326'}, 'crs': {'properties': {'name': 'EPSG4326'}, 'type': 'name'}}) '\x00 \x00\x00\x01\x00\x00\x10\xe6@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00' GeoPackage binary supports encoding of SRID and envelope information. If your geopackage has an envelope specified, then it will be added into the resulting GeoJSON in a key called `'bbox'`: >>> gpkg = b'GP\x00\x03\x00\x00\x00\x00\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@\x01\x01\x00\x00\x00\xf0\x9e\xa0\xa7\x05;#@hZ\xbd\x93\x83GC@' >>> geopackage.loads(gpkg) >>> {'type': 'Point', 'coordinates': [9.615277517659223, 38.55870291467437], 'bbox': (9.615277517659223, 38.55870291467437, 9.615277517659223, 38.55870291467437)} In the same way, if a 'bbox' key is present on a `dumps`-ed geometry, it will be added to the header of the GeoPackage geometry: >>> polygon = {'type': 'Polygon', 'coordinates': [[[20.0, 20.0], [34.0, 124.0], [70.0, 140.0], [130.0, 130.0], [70.0, 100.0], [110.0, 70.0], [170.0, 20.0], [90.0, 10.0], [20.0, 20.0]]], 'bbox': (20.0, 170.0, 10.0, 140.0)} >>> geopackage.dumps(polygon) b'GP\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00@e@\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x80a@\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\t@4\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@A\x00\x00\x00\x00\x00\x00@_\x00\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@a\x80\x00\x00\x00\x00\x00@`@\x00\x00\x00\x00\x00@`@\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@Y\x00\x00\x00\x00\x00\x00@[\x80\x00\x00\x00\x00\x00@Q\x80\x00\x00\x00\x00\x00@e@\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@V\x80\x00\x00\x00\x00\x00@$\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00@4\x00\x00\x00\x00\x00\x00' If an integer SRID identifier is present in a `'meta'` key (like `'meta': {'srid': 4326}`), then the SRID will be included in the GeoPackage header. ## History This library was originally created as the result of a bug report related to another project: https://bugs.launchpad.net/openquake-old/+bug/1073909. The source of this issue was largely due to a dependency on [GEOS](https://libgeos.org/), which is written in C/C++. Depending on GEOS requires any data conversion bug fixes to happen upstream, which takes time and effort. Ultimately, this was the inspiration to create a more lightweight, pure-Python conversion library as an alterntive tool for reliably converting data between various geospatial formats. The name "GeoMet" was inspired by "met", the German word for [mead](http://en.wikipedia.org/wiki/Mead). It is also a shortened version of the word "geometry". ## Limitations ### Outputing "empty" geometries to binary formats is not supported Attempting to output an empty geometry to a binary format will result in an exception: `ValueError: Empty geometries cannot be represented in WKB. Reason: The dimensionality of the WKB would be ambiguous.` There are a few reasons for this this limitation: - Any `EMTPY` geometry (e.g., `POINT EMPTY`, `MULTIPOLYGON EMPTY`, etc.) cannot be converted into binary format because binary formats such as WKB require an explicit dimension type (2d, Z, M, or ZM). This means that some objects cannot be reliably converted to and from different formats in a [bijective](https://en.wikipedia.org/wiki/Bijection) manner. - The [GeoJSON standard](https://www.rfc-editor.org/rfc/rfc7946) does have a way of representing empty geometries; however, details are minimal and the dimensionality of such an object remains ambiguous. - Representing some geometry types (such as points and lines) as "empty" is [deeply flawed to begin with](http://aleph0.clarku.edu/~djoyce/elements/bookI/defI1.html). For example, a point can represent any location in 2d, 3d, or 4d space. However, a point is infinitesimally small (it has no size) and it can't contain anything (it can't be "full"), therefore, it doesn't make sense for a point to be "empty". As a result, GeoMet has chosen to not attempt to address these problems, and simply raise an exception instead. Example: >>> import geomet >>> import geomet.wkt as wkt >>> import geomet.wkb as wkb >>> pt = wkt.loads('POINT EMPTY') >>> pt {'type': 'Point', 'coordinates': []} >>> wkb.dumps(pt) Traceback (most recent call last): File "", line 1, in File "/home/jdoe/geomet/geomet/wkb.py", line 216, in dumps return _dumps(obj, big_endian) File "/home/jdoe/geomet/geomet/wkb.py", line 238, in _dumps raise ValueError( ValueError: Empty geometries cannot be represented in WKB. Reason: The dimensionality of the WKB would be ambiguous. ## See also - [wellknown](https://github.com/mapbox/wellknown): A similar package for Node.js. - [geo](https://github.com/bryanjos/geo): A nearly-identical package for Elixir. geomet-1.1.0/geomet.egg-info/SOURCES.txt000066400000000000000000000005341453626627600176520ustar00rootroot00000000000000AUTHORS.txt LICENSE README.md pyproject.toml setup.py geomet/__init__.py geomet/esri.py geomet/geopackage.py geomet/tool.py geomet/util.py geomet/wkb.py geomet/wkt.py geomet.egg-info/PKG-INFO geomet.egg-info/SOURCES.txt geomet.egg-info/dependency_links.txt geomet.egg-info/entry_points.txt geomet.egg-info/requires.txt geomet.egg-info/top_level.txtgeomet-1.1.0/geomet.egg-info/dependency_links.txt000066400000000000000000000000011453626627600220320ustar00rootroot00000000000000 geomet-1.1.0/geomet.egg-info/entry_points.txt000066400000000000000000000000531453626627600212600ustar00rootroot00000000000000[console_scripts] geomet = geomet.tool:cli geomet-1.1.0/geomet.egg-info/requires.txt000066400000000000000000000000061453626627600203600ustar00rootroot00000000000000click geomet-1.1.0/geomet.egg-info/top_level.txt000066400000000000000000000000071453626627600205130ustar00rootroot00000000000000geomet geomet-1.1.0/geomet/000077500000000000000000000000001453626627600142725ustar00rootroot00000000000000geomet-1.1.0/geomet/__init__.py000066400000000000000000000013771453626627600164130ustar00rootroot00000000000000# Copyright 2013 Lars Butler & individual contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. __version__ = '1.1.0' class InvalidGeoJSONException(Exception): """ Simple exception class to indicate if invalid GeoJSON is encountered. """ geomet-1.1.0/geomet/esri.py000066400000000000000000000154601453626627600156140ustar00rootroot00000000000000# Copyright 2013 Lars Butler & individual contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import geomet def load(source_file): """ Converts Esri Json File to GeoJSON. :param source_file: Path to a file that contains the Esri JSON data. :returns: A GeoJSON `dict` representing the geometry read from the file. """ return json.load(source_file) def loads(string): """ Construct a GeoJSON `dict` from Esri JSON (string/dict). :param string: The Esri JSON geometry representation :returns: A GeoJSON `dict` representing the geometry read from the file. """ data = json.loads(string) if "rings" in data: return _esri_to_geojson_convert["rings"](data) elif "paths" in data: return _esri_to_geojson_convert["paths"](data) elif "x" in data or "y" in data: return _esri_to_geojson_convert["x"](data) elif "points" in data: return _esri_to_geojson_convert["points"](data) else: raise geomet.InvalidGeoJSONException("Invalid EsriJSON: %s" % string) def dump(obj, dest_file, srid=None): """ Converts GeoJSON to Esri JSON File. """ return json.dump(dumps(obj, srid=srid), dest_file) def dumps(obj, srid=None): """ Dump a GeoJSON-like `dict` to a Esri JSON. :param string: The GeoJSON geometry representation :param int: The default SRID value if none is present. """ if "type" in obj and obj["type"].lower() in _gj_to_esri.keys(): convert = _gj_to_esri[obj["type"].lower()] return convert(obj, srid=srid) else: raise geomet.InvalidGeoJSONException("Invalid GeoJSON type %s" % obj) def _extract_geojson_srid(obj): """ Extracts the SRID code (WKID code) from geojson. If not found, SRID=4326 :returns: Integer """ meta_srid = obj.get("meta", {}).get("srid", None) # Also try to get it from `crs.properties.name`: crs_srid = obj.get("crs", {}).get("properties", {}).get("name", None) if crs_srid is not None: # Shave off the EPSG: prefix to give us the SRID: crs_srid = crs_srid.replace("EPSG:", "") if ( meta_srid is not None and crs_srid is not None and str(meta_srid) != str(crs_srid) ): raise ValueError( "Ambiguous CRS/SRID values: %s and %s" % (meta_srid, crs_srid) ) srid = meta_srid or crs_srid return srid or 4326 def _dump_geojson_point(obj, srid=None): """ Loads GeoJSON to Esri JSON for Geometry type Point. """ coordkey = "coordinates" coords = obj[coordkey] if srid is None: srid = _extract_geojson_srid(obj) return {"x": coords[0], "y": coords[1], "spatialReference": {"wkid": srid}} def _dump_geojson_multipoint(obj, srid=None): """ Loads GeoJSON to Esri JSON for Geometry type MultiPoint. """ coordkey = "coordinates" if srid is None: srid = _extract_geojson_srid(obj) return {"points": obj[coordkey], "spatialReference": {"wkid": srid}} def _dump_geojson_polyline(obj, srid=None): """ Loads GeoJSON to Esri JSON for Geometry type LineString and MultiLineString. """ coordkey = "coordinates" if obj["type"].lower() == "linestring": coordinates = [obj[coordkey]] else: coordinates = obj[coordkey] if srid is None: srid = _extract_geojson_srid(obj) return {"paths": coordinates, "spatialReference": {"wkid": srid}} def _dump_geojson_polygon(data, srid=None): """ Loads GeoJSON to Esri JSON for Geometry type Polygon or MultiPolygon. """ coordkey = "coordinates" coordinates = data[coordkey] typekey = ([d for d in data if d.lower() == "type"] or ["type"]).pop() if data[typekey].lower() == "polygon": coordinates = [coordinates] part_list = [] for part in coordinates: if len(part) == 1: part_list.append(part[0]) else: for seg in part: part_list.append([list(coord) for coord in seg]) if srid is None: srid = _extract_geojson_srid(data) return {"rings": part_list, "spatialReference": {"wkid": srid}} def _to_gj_point(obj): """ Dump a Esri JSON Point to GeoJSON Point. :param dict obj: A EsriJSON-like `dict` representing a Point. :returns: GeoJSON representation of the Esri JSON Point """ if obj.get("x", None) is None or obj.get("y", None) is None: return {"type": "Point", "coordinates": ()} return {"type": "Point", "coordinates": (obj.get("x"), obj.get("y"))} def _to_gj_polygon(obj): """ Dump a EsriJSON-like Polygon object to GeoJSON. Input parameters and return value are the POLYGON equivalent to :func:`_to_gj_point`. """ def split_part(a_part): part_list = [] for item in a_part: if item is None: if part_list: yield part_list part_list = [] else: part_list.append((item[0], item[1])) if part_list: yield part_list part_json = [list(split_part(part)) for part in obj["rings"]] return {"type": "MultiPolygon", "coordinates": part_json} def _to_gj_multipoint(data): """ Dump a EsriJSON-like MultiPoint object to GeoJSON-dict. Input parameters and return value are the MULTIPOINT equivalent to :func:`_to_gj_point`. :returns: `dict` """ return {"type": "MultiPoint", "coordinates": [pt for pt in data["points"]]} def _to_gj_polyline(data): """ Dump a GeoJSON-like MultiLineString object to WKT. Input parameters and return value are the MULTILINESTRING equivalent to :func:`_dump_point`. """ return { "type": "MultiLineString", "coordinates": [ [((pt[0], pt[1]) if pt else None) for pt in part] for part in data["paths"] ], } _esri_to_geojson_convert = { "x": _to_gj_point, "y": _to_gj_point, "points": _to_gj_multipoint, "rings": _to_gj_polygon, "paths": _to_gj_polyline, } _gj_to_esri = { "point": _dump_geojson_point, "multipoint": _dump_geojson_multipoint, "linestring": _dump_geojson_polyline, "multilinestring": _dump_geojson_polyline, "polygon": _dump_geojson_polygon, "multipolygon": _dump_geojson_polygon, } geomet-1.1.0/geomet/geopackage.py000066400000000000000000000274761453626627600167520ustar00rootroot00000000000000# Copyright 2020 Tom Caruso & individual contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import struct as _struct from geomet.util import ( as_bin_str as _as_bin_str, take as _take, endian_token as _endian_token ) from geomet import wkb as _wkb def dump(obj, dest_file, big_endian=True): """ Dump GeoJSON-like `dict` to GeoPackage binary and write it to the `dest_file`. :param dict obj: A GeoJSON-like dictionary. It must at least the keys 'type' and 'coordinates'. :param dest_file: Open and writable file-like object. :param bool big_endian: specify endianess of the dumped object. :return: """ dest_file.write(dumps(obj, big_endian)) def load(source_file): """ Load a GeoJSON `dict` object from a ``source_file`` containing GeoPackage (as a byte string). :param source_file: Open and readable file-like object. :return: A GeoJSON `dict` representing the geometry read from the file. """ return loads(source_file.read()) def dumps(obj, big_endian=True): """ Dump a GeoJSON-like dict to a GeoPackage bytestring. If the dict contains a top-level 'meta' key like so: ``` 'meta': {'srid': 4326} ``` then the srid will be added to the geopackage header, but *not* to the WKB geometry header. If the dict contains a top-level 'bbox' key like so: ``` 'bbox': [0, 0, 3, 3] ``` Then an envelope will be added to the geopackage header with this information. If the geometry's coordinates are empty (an empty list) then the geopackage header's "empty" flag will be set, denoting that this geometry has no coordinates. Please note that while this library can parse geopackages with a mixed byte-order in the header, it will only produce blobs with consistent byte order (albeit properly marked as such). That means you cannot product a geopackage with e.g. little-endian header and big-endian WKB geometry. :param dict obj: The geojson geometry to dump :param bool big_endian: if True, the geopackage binary will use big-endian byte order, little-endian otherwise. :return bytes: bytestring representing the geometry in geopackage format. """ header = _build_geopackage_header(obj, not big_endian) result = _wkb._dumps(obj, big_endian, include_meta=False) return header + result def loads(string): """ Construct a GeoJSON `dict` from geopackage (string). This function strips the geopackage header from the string and passes the remaining WKB geometry to the `geomet.wkb.loads` function. The envelope, if present, is added to the GeoJSON as a key called 'bbox' as per the GeoJSON spec, [1]. If an SRID is specified in the geopackage header AND the wkb header, the SRID in the geopackage header will take precedence and will replace that SRID in the returned dict. [1] https://tools.ietf.org/html/rfc7946#section-5 :param bytes string: geopackage byte string. :return dict: GeoJSON represented the parsed geopackage binary. """ string = iter(string) header = _as_bin_str(_take(_GeoPackage.HEADER_LEN, string)) _check_is_valid(header) g, p, version, empty, envelope_indicator, is_little_endian, srid = ( _parse_header(header) ) wkb_offset = _get_wkb_offset(envelope_indicator) left_to_take = (wkb_offset - _GeoPackage.HEADER_LEN) envelope_data = _as_bin_str(_take(left_to_take, string)) if envelope_data: envelope = _parse_envelope( envelope_indicator, envelope_data, is_little_endian ) result = _wkb.loads(string) if srid: result['meta'] = {'srid': int(srid)} result['crs'] = { 'type': 'name', 'properties': {'name': 'EPSG%s' % srid}, } if envelope_data: result['bbox'] = envelope return result class _GeoPackage: """ Much more information on geopackage structure can be found here: http://www.geopackage.org/spec/#gpb_format """ # The ascii letter 'G' MAGIC1 = 0x47 # The ascii letter 'P' MAGIC2 = 0x50 VERSION1 = 0x00 HEADER_LEN = 8 HEADER_PACK_FMT = "BBBBI" ENVELOPE_2D_LEN = 32 ENVELOPE_3D_LEN = 48 ENVELOPE_4D_LEN = 64 ENVELOPE_MASK = 0b00001111 EMPTY_GEOM_MASK = 0b00011111 ENDIANNESS_MASK = 0b00000001 # map the "envelope indicator" integer we get out of the geopackage header # to the dimensionality of the envelope. # more info here: http://www.geopackage.org/spec/#gpb_format # in the "flags" section, bits 3, 2, 1. _indicator_to_dim = { 0: 0, 1: 4, 2: 6, 3: 6, 4: 8, } # Map the dimensionality of our envelope to the indicator # integer we will use in the geopackage binary header. # because we have no way to tell between Z and M values, # if the geometry has 3 dimensions we default to assume Z. _dim_to_indicator = { 0: 0, 4: 1, 6: 2, 8: 4 } def is_valid(data): """ Check if the data represents a valid geopackage geometry. Input can be either the full geometry or just the header. :param bytes data: bytes representing the geopackage binary. :return (bool, str): Is the geopackage valid, if not, string describing why """ g, p, version, _, envelope_indicator, _, _ = _parse_header(data[:8]) if (g != _GeoPackage.MAGIC1) or (p != _GeoPackage.MAGIC2): return False, "Missing Geopackage header magic bytes" if version != _GeoPackage.VERSION1: return False, "Geopackage version must be 0" if (envelope_indicator < 0) or (envelope_indicator > 4): return False, "Envelope indicator must be between 0-4" return True, "" def _header_is_little_endian(header): """ Check to see if the header is encoded as little endian or big endian. Either the entire binary blob or just the header can be passed in. :param bytes header: geopackage header or binary blob :return bool: is the header little endian """ (flags,) = _struct.unpack("B", header[3:4]) return flags & _GeoPackage.ENDIANNESS_MASK def _parse_header(header): """ Unpack all information from the geopackage header, including "magic" GP bytes. Returns all of them so we can confirm that this geopackage is validly formed. Can also accept the full binary blob. :param header: the header or the full geometry. :return 7-tuple: all attributes stored in the binary header. """ is_little_endian = _header_is_little_endian(header) fmt = _endian_token(is_little_endian) + _GeoPackage.HEADER_PACK_FMT g, p, version, flags, srid = _struct.unpack( fmt, header[:_GeoPackage.HEADER_LEN] ) empty, envelope_indicator, endianness = _parse_flags(flags) return g, p, version, empty, envelope_indicator, endianness, srid def _parse_flags(flags): """ Parse the bits in the "flags" byte of the geopackage header to retrieve useful information. We specifically parse the endianness, the envelope indicator, and the "empty" flag. Much more info can be found in the documentation [1]. [1] http://www.geopackage.org/spec/#gpb_format :param byte flags: The "flags" byte of a geopackage header. :return tuple: """ endianness = flags & _GeoPackage.ENDIANNESS_MASK envelope_indicator = (flags & _GeoPackage.ENVELOPE_MASK) >> 1 empty = (flags & _GeoPackage.EMPTY_GEOM_MASK) >> 4 return empty, envelope_indicator, endianness def _build_flags(empty, envelope_indicator, is_little_endian=1): """ Create the "flags" byte which goes into the geopackage header. Much more info can be found in the documentation [1]. [1] http://www.geopackage.org/spec/#gpb_format :param int empty: 0 or 1 indicating whether the geometry is empty. True and False also work as expected. :param int envelope_indicator: indicates the dimensionality of the envelope. :param int is_little_endian: 0 or 1 (or False / True) indicating whether the header should be little-endian encoded. :return byte: geopackage header flags """ flags = 0b0 if empty: flags = (flags | 1) << 3 if envelope_indicator: flags = flags | envelope_indicator return (flags << 1) | is_little_endian def _build_geopackage_header(obj, is_little_endian): """ Create the geopackage header for the input object. Looks for a 'bbox' key on the geometry to use for an envelope, and a 'meta' key with an SRID to encode into the header. :param dict obj: a geojson object :param bool is_little_endian: which endianness to use when encoding the data. :return bytes: geopackage header. """ # Collect geometry metadata. empty = 1 if len(obj['coordinates']) == 0 else 0 envelope = obj.get('bbox', []) srid = obj.get('meta', {}).get('srid', 0) try: envelope_indicator = _dim_to_indicator[len(envelope)] except KeyError: raise ValueError("Bounding box must be of length 2*n where " "n is the number of dimensions represented " "in the contained geometries.") pack_args = [ _GeoPackage.MAGIC1, _GeoPackage.MAGIC2, _GeoPackage.VERSION1, # This looks funny, but _build_flags wants a 1 or 0 for # "little endian" because it uses it to `or` with the bits. # Conveniently, in Python, False == 0 and True == 1, so # we can pass the boolean right in and it works as expected. _build_flags(empty, envelope_indicator, is_little_endian), srid ] pack_fmt = _endian_token(is_little_endian) + _GeoPackage.HEADER_PACK_FMT # This has no effect if we have a 0 envelope indicator. pack_fmt += ('d' * _indicator_to_dim[envelope_indicator]) pack_args.extend(envelope) return _struct.pack(pack_fmt, *pack_args) def _check_is_valid(data): """ Raise if the header is not valid geopackage. :param bytes data: Geopackage data or header. :return None: """ valid, reason = is_valid(data) if not valid: raise ValueError("Could not read Geopackage geometry " "because of errors: " + reason) def _get_wkb_offset(envelope_indicator): """ Get the full byte offset at which the WKB geometry lies in the geopackage geometry. :param int envelope_indicator: indicates the dimensionality of the envelope. :return int: number of bytes until the beginning of the WKB geometry. """ base_len = _GeoPackage.HEADER_LEN return (base_len * _indicator_to_dim[envelope_indicator]) + base_len def _parse_envelope(envelope_indicator, envelope, is_little_endian): """ Parse a geopackage envelope bytestring into an n-tuple of floats. :param int envelope_indicator: indicates the dimensionality of the envelope. :param bytes envelope: Bytestring of the envelope values. :param bool is_little_endian: how to pack the bytes in the envelope. :return tuple[float]: Geometry envelope. """ pack_fmt = _endian_token(is_little_endian) pack_fmt += ('d' * _indicator_to_dim[envelope_indicator]) return _struct.unpack(pack_fmt, envelope) geomet-1.1.0/geomet/tool.py000066400000000000000000000073241453626627600156270ustar00rootroot00000000000000#!/usr/bin/env python # Copyright 2013 Lars Butler & individual contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Simple CLI for converting between WKB/WKT and GeoJSON Example usage: $ echo "POINT (0.9999999 0.9999999)" \ > | geomet --wkb - \ > | geomet --wkt --precision 7 - POINT (0.9999999 0.9999999) """ from binascii import a2b_hex from binascii import b2a_hex import json import logging import sys import click from geomet import util, wkb, wkt CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) def configure_logging(verbosity): log_level = max(10, 30 - 10 * verbosity) logging.basicConfig(stream=sys.stderr, level=log_level) def translate(text, output_format='json', indent=None, precision=-1): if text.startswith('{'): geom = json.loads(text) elif text.startswith(('G', 'L', 'M', 'P')): geom = wkt.loads(text) else: geom = wkb.loads(a2b_hex(text)) if output_format == 'wkb': output = b2a_hex(wkb.dumps(geom)) elif output_format == 'wkt': kwds = {} if precision >= 0: kwds['decimals'] = precision output = wkt.dumps(geom, **kwds) else: if precision >= 0: geom = util.round_geom(geom, precision) output = json.dumps(geom, indent=indent, sort_keys=True) return output @click.command( short_help="Convert between WKT or hex-encoded WKB and GeoJSON.", context_settings=CONTEXT_SETTINGS) @click.argument('input', default='-', required=False) @click.option('--verbose', '-v', count=True, help="Increase verbosity.") @click.option('--quiet', '-q', count=True, help="Decrease verbosity.") @click.option('--json', 'output_format', flag_value='json', default=True, help="JSON output.") @click.option('--wkb', 'output_format', flag_value='wkb', help="Hex-encoded WKB output.") @click.option('--wkt', 'output_format', flag_value='wkt', help="WKT output.") @click.option('--precision', type=int, default=-1, help="Decimal precision of JSON and WKT coordinates.") @click.option('--indent', default=None, type=int, help="Indentation level for pretty printed output") def cli(input, verbose, quiet, output_format, precision, indent): """Convert text read from the first positional argument, stdin, or a file to GeoJSON and write to stdout.""" verbosity = verbose - quiet configure_logging(verbosity) logger = logging.getLogger('geomet') # Handle the case of file, stream, or string input. try: src = click.open_file(input).readlines() except IOError: src = [input] stdout = click.get_text_stream('stdout') # Read-write loop. try: for line in src: text = line.strip() logger.debug("Input: %r", text) output = translate( text, output_format=output_format, indent=indent, precision=precision ) logger.debug("Output: %r", output) stdout.write(output) stdout.write('\n') sys.exit(0) except Exception: logger.exception("Failed. Exception caught") sys.exit(1) if __name__ == '__main__': cli() geomet-1.1.0/geomet/util.py000066400000000000000000000077541453626627600156360ustar00rootroot00000000000000# Copyright 2013 Lars Butler & individual contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import itertools import collections.abc as collections def block_splitter(data, block_size): """ Creates a generator by slicing ``data`` into chunks of ``block_size``. >>> data = range(10) >>> list(block_splitter(data, 2)) [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]] If ``data`` cannot be evenly divided by ``block_size``, the last block will simply be the remainder of the data. Example: >>> data = range(10) >>> list(block_splitter(data, 3)) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] If the ``block_size`` is greater than the total length of ``data``, a single block will be generated: >>> data = range(3) >>> list(block_splitter(data, 4)) [[0, 1, 2]] :param data: Any iterable. If ``data`` is a generator, it will be exhausted, obviously. :param int block_site: Desired (maximum) block size. """ buf = [] for i, datum in enumerate(data): buf.append(datum) if len(buf) == block_size: yield buf buf = [] # If there's anything leftover (a partial block), # yield it as well. if buf: yield buf def take(n, iterable): """ Return first n items of the iterable as a list Copied shamelessly from http://docs.python.org/2/library/itertools.html#recipes. """ return list(itertools.islice(iterable, n)) def as_bin_str(a_list): return bytes(a_list) def round_geom(geom, precision=None): """Round coordinates of a geometric object to given precision.""" if geom['type'] == 'Point': x, y = geom['coordinates'] xp, yp = [x], [y] if precision is not None: xp = [round(v, precision) for v in xp] yp = [round(v, precision) for v in yp] new_coords = tuple(zip(xp, yp))[0] if geom['type'] in ['LineString', 'MultiPoint']: xp, yp = zip(*geom['coordinates']) if precision is not None: xp = [round(v, precision) for v in xp] yp = [round(v, precision) for v in yp] new_coords = tuple(zip(xp, yp)) elif geom['type'] in ['Polygon', 'MultiLineString']: new_coords = [] for piece in geom['coordinates']: xp, yp = zip(*piece) if precision is not None: xp = [round(v, precision) for v in xp] yp = [round(v, precision) for v in yp] new_coords.append(tuple(zip(xp, yp))) elif geom['type'] == 'MultiPolygon': parts = geom['coordinates'] new_coords = [] for part in parts: inner_coords = [] for ring in part: xp, yp = zip(*ring) if precision is not None: xp = [round(v, precision) for v in xp] yp = [round(v, precision) for v in yp] inner_coords.append(tuple(zip(xp, yp))) new_coords.append(inner_coords) return {'type': geom['type'], 'coordinates': new_coords} def flatten_multi_dim(sequence): """Flatten a multi-dimensional array-like to a single dimensional sequence (as a generator). """ for x in sequence: if (isinstance(x, collections.Iterable) and not isinstance(x, str)): for y in flatten_multi_dim(x): yield y else: yield x def endian_token(is_little_endian): if is_little_endian: return '<' else: return '>' geomet-1.1.0/geomet/wkb.py000066400000000000000000000735631453626627600154450ustar00rootroot00000000000000# Copyright 2013 Lars Butler & individual contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import binascii import struct from geomet.util import block_splitter from geomet.util import take from geomet.util import as_bin_str from geomet.util import flatten_multi_dim from itertools import chain #: '\x00': The first byte of any WKB string. Indicates big endian byte #: ordering for the data. BIG_ENDIAN = b'\x00' #: '\x01': The first byte of any WKB string. Indicates little endian byte #: ordering for the data. LITTLE_ENDIAN = b'\x01' #: High byte in a 4-byte geometry type field to indicate that a 4-byte SRID #: field follows. SRID_FLAG = b'\x20' #: Mapping of GeoJSON geometry types to the "2D" 4-byte binary string #: representation for WKB. "2D" indicates that the geometry is 2-dimensional, #: X and Y components. #: NOTE: Byte ordering is big endian. WKB_2D = { 'Point': b'\x00\x00\x00\x01', 'LineString': b'\x00\x00\x00\x02', 'Polygon': b'\x00\x00\x00\x03', 'MultiPoint': b'\x00\x00\x00\x04', 'MultiLineString': b'\x00\x00\x00\x05', 'MultiPolygon': b'\x00\x00\x00\x06', 'GeometryCollection': b'\x00\x00\x00\x07', } #: Mapping of GeoJSON geometry types to the "Z" 4-byte binary string #: representation for WKB. "Z" indicates that the geometry is 3-dimensional, #: with X, Y, and Z components. #: NOTE: Byte ordering is big endian. WKB_Z = { 'Point': b'\x00\x00\x03\xe9', 'LineString': b'\x00\x00\x03\xea', 'Polygon': b'\x00\x00\x03\xeb', 'MultiPoint': b'\x00\x00\x03\xec', 'MultiLineString': b'\x00\x00\x03\xed', 'MultiPolygon': b'\x00\x00\x03\xee', 'GeometryCollection': b'\x00\x00\x03\xef', } #: Mapping of GeoJSON geometry types to the "M" 4-byte binary string #: representation for WKB. "M" indicates that the geometry is 2-dimensional, #: with X, Y, and M ("Measure") components. #: NOTE: Byte ordering is big endian. WKB_M = { 'Point': b'\x00\x00\x07\xd1', 'LineString': b'\x00\x00\x07\xd2', 'Polygon': b'\x00\x00\x07\xd3', 'MultiPoint': b'\x00\x00\x07\xd4', 'MultiLineString': b'\x00\x00\x07\xd5', 'MultiPolygon': b'\x00\x00\x07\xd6', 'GeometryCollection': b'\x00\x00\x07\xd7', } #: Mapping of GeoJSON geometry types to the "ZM" 4-byte binary string #: representation for WKB. "ZM" indicates that the geometry is 4-dimensional, #: with X, Y, Z, and M ("Measure") components. #: NOTE: Byte ordering is big endian. WKB_ZM = { 'Point': b'\x00\x00\x0b\xb9', 'LineString': b'\x00\x00\x0b\xba', 'Polygon': b'\x00\x00\x0b\xbb', 'MultiPoint': b'\x00\x00\x0b\xbc', 'MultiLineString': b'\x00\x00\x0b\xbd', 'MultiPolygon': b'\x00\x00\x0b\xbe', 'GeometryCollection': b'\x00\x00\x0b\xbf', } #: Mapping of dimension types to maps of GeoJSON geometry type -> 4-byte binary #: string representation for WKB. _WKB = { '2D': WKB_2D, 'Z': WKB_Z, 'M': WKB_M, 'ZM': WKB_ZM, } #: Mapping from binary geometry type (as a 4-byte binary string) to GeoJSON #: geometry type. #: NOTE: Byte ordering is big endian. _BINARY_TO_GEOM_TYPE = dict( chain(*((reversed(x) for x in wkb_map.items()) for wkb_map in _WKB.values())) ) _INT_TO_DIM_LABEL = {2: '2D', 3: 'Z', 4: 'ZM'} def _get_geom_type(type_bytes): """Get the GeoJSON geometry type label from a WKB type byte string. :param type_bytes: 4 byte string in big endian byte order containing a WKB type number. It may also contain a "has SRID" flag in the high byte (the first type, since this is big endian byte order), indicated as 0x20. If the SRID flag is not set, the high byte will always be null (0x00). :returns: 3-tuple ofGeoJSON geometry type label, the bytes representing the geometry type, and a separate "has SRID" flag. If the input `type_bytes` contains an SRID flag, it will be removed. >>> # Z Point, with SRID flag >>> _get_geom_type(b'\\x20\\x00\\x03\\xe9') == ( ... 'Point', b'\\x00\\x00\\x03\\xe9', True) True >>> # 2D MultiLineString, without SRID flag >>> _get_geom_type(b'\\x00\\x00\\x00\\x05') == ( ... 'MultiLineString', b'\\x00\\x00\\x00\\x05', False) True """ # slice off the high byte, which may contain the SRID flag high_byte = bytes([type_bytes[0]]) has_srid = high_byte == b'\x20' if has_srid: # replace the high byte with a null byte type_bytes = as_bin_str(b'\x00' + type_bytes[1:]) else: type_bytes = as_bin_str(type_bytes) # look up the geometry type geom_type = _BINARY_TO_GEOM_TYPE.get(type_bytes) return geom_type, type_bytes, has_srid def dump(obj, dest_file): """ Dump GeoJSON-like `dict` to WKB and write it to the `dest_file`. :param dict obj: A GeoJSON-like dictionary. It must at least the keys 'type' and 'coordinates'. :param dest_file: Open and writable file-like object. """ dest_file.write(dumps(obj)) def load(source_file): """ Load a GeoJSON `dict` object from a ``source_file`` containing WKB (as a byte string). :param source_file: Open and readable file-like object. :returns: A GeoJSON `dict` representing the geometry read from the file. """ return loads(source_file.read()) def dumps(obj, big_endian=True): """ Dump a GeoJSON-like `dict` to a WKB string. .. note:: The dimensions of the generated WKB will be inferred from the first vertex in the GeoJSON `coordinates`. It will be assumed that all vertices are uniform. There are 4 types: - 2D (X, Y): 2-dimensional geometry - Z (X, Y, Z): 3-dimensional geometry - M (X, Y, M): 2-dimensional geometry with a "Measure" - ZM (X, Y, Z, M): 3-dimensional geometry with a "Measure" If the first vertex contains 2 values, we assume a 2D geometry. If the first vertex contains 3 values, this is slightly ambiguous and so the most common case is chosen: Z. If the first vertex contains 4 values, we assume a ZM geometry. The WKT/WKB standards provide a way of differentiating normal (2D), Z, M, and ZM geometries (http://en.wikipedia.org/wiki/Well-known_text), but the GeoJSON spec does not. Therefore, for the sake of interface simplicity, we assume that geometry that looks 3D contains XYZ components, instead of XYM. If the coordinates list has no coordinate values (this includes nested lists, for example, `[[[[],[]], []]]`, the geometry is considered to be empty. Geometries, with the exception of points, have a reasonable "empty" representation in WKB; however, without knowing the number of coordinate values per vertex, the type is ambigious, and thus we don't know if the geometry type is 2D, Z, M, or ZM. Therefore in this case we expect a `ValueError` to be raised. :param dict obj: GeoJson-like `dict` object. :param bool big_endian: Defaults to `True`. If `True`, data values in the generated WKB will be represented using big endian byte order. Else, little endian. :returns: A WKB binary string representing of the ``obj``. """ return _dumps(obj, big_endian) def _dumps(obj, big_endian=True, include_meta=True): """ Basically perform the action of dumps, but with some extra flags for behavior specifically needed by the geopackage...package. """ geom_type = obj['type'] if include_meta: meta = obj.get('meta', {}) else: meta = {} exporter = _dumps_registry.get(geom_type) if exporter is None: _unsupported_geom_type(geom_type) # Check for empty geometries. GeometryCollections have a slightly different # JSON/dict structure, but that's handled. coords_or_geoms = obj.get('coordinates', obj.get('geometries')) if len(list(flatten_multi_dim(coords_or_geoms))) == 0: raise ValueError( 'Empty geometries cannot be represented in WKB. Reason: The ' 'dimensionality of the WKB would be ambiguous.' ) return exporter(obj, big_endian, meta) def loads(string): """ Construct a GeoJSON `dict` from WKB (`string`). The resulting GeoJSON `dict` will include the SRID as an integer in the `meta` object. This was an arbitrary decision made by `geomet, the discussion of which took place here: https://github.com/geomet/geomet/issues/28. In order to be consistent with other libraries [1] and (deprecated) specifications [2], also include the same information in a `crs` object. This isn't ideal, but the `crs` member is no longer part of the GeoJSON standard, according to RFC7946 [3]. However, it's still useful to include this information in GeoJSON payloads because it supports conversion to EWKT/EWKB (which are canonical formats used by PostGIS and the like). Example: {'type': 'Point', 'coordinates': [0.0, 1.0], 'meta': {'srid': 4326}, 'crs': {'type': 'name', 'properties': {'name': 'EPSG4326'}}} NOTE(larsbutler): I'm not sure if it's valid to just prefix EPSG (European Petroluem Survey Group) to an SRID like this, but we'll stick with it for now until it becomes a problem. NOTE(larsbutler): Ideally, we should use URNs instead of this notation, according to the new GeoJSON spec [4]. However, in order to be consistent with [1], we'll stick with this approach for now. References: [1] - https://github.com/bryanjos/geo/issues/76 [2] - http://geojson.org/geojson-spec.html#coordinate-reference-system-objects [3] - https://tools.ietf.org/html/rfc7946#appendix-B.1 [4] - https://tools.ietf.org/html/rfc7946#section-4 """ # noqa string = iter(string) # endianness = string[0:1] endianness = as_bin_str(take(1, string)) if endianness == BIG_ENDIAN: big_endian = True elif endianness == LITTLE_ENDIAN: big_endian = False else: raise ValueError("Invalid endian byte: '0x%s'. Expected 0x00 or 0x01" % binascii.hexlify(endianness.encode()).decode()) endian_token = '>' if big_endian else '<' # type_bytes = string[1:5] type_bytes = as_bin_str(take(4, string)) if not big_endian: # To identify the type, order the type bytes in big endian: type_bytes = type_bytes[::-1] geom_type, type_bytes, has_srid = _get_geom_type(type_bytes) srid = None if has_srid: srid_field = as_bin_str(take(4, string)) [srid] = struct.unpack('%si' % endian_token, srid_field) # data_bytes = string[5:] # FIXME: This won't work for GeometryCollections data_bytes = string importer = _loads_registry.get(geom_type) if importer is None: _unsupported_geom_type(geom_type) data_bytes = iter(data_bytes) result = importer(big_endian, type_bytes, data_bytes) if has_srid: # As mentioned in the docstring above, include both approaches to # indicating the SRID. result['meta'] = {'srid': int(srid)} result['crs'] = { 'type': 'name', 'properties': {'name': 'EPSG%s' % srid}, } return result def _unsupported_geom_type(geom_type): raise ValueError("Unsupported geometry type '%s'" % geom_type) # TODO: dont default meta to none def _header_bytefmt_byteorder(geom_type, num_dims, big_endian, meta=None): """ Utility function to get the WKB header (endian byte + type header), byte format string, and byte order string. """ dim = _INT_TO_DIM_LABEL.get(num_dims) if dim is None: pass # TODO: raise type_byte_str = _WKB[dim][geom_type] srid = meta.get('srid') if srid is not None: # Add the srid flag type_byte_str = SRID_FLAG + type_byte_str[1:] if big_endian: header = BIG_ENDIAN byte_fmt = b'>' byte_order = '>' else: header = LITTLE_ENDIAN byte_fmt = b'<' byte_order = '<' # reverse the byte ordering for little endian type_byte_str = type_byte_str[::-1] header += type_byte_str if srid is not None: srid = int(srid) if big_endian: srid_header = struct.pack('>i', srid) else: srid_header = struct.pack(' 0: coords.append(pt) return dict(type='MultiPoint', coordinates=coords) def _load_multipolygon(tokens, string): """ Has similar inputs and return value to to :func:`_load_point`, except is for handling MULTIPOLYGON geometry. :returns: A GeoJSON `dict` MultiPolygon representation of the WKT ``string``. """ next_token = next(tokens) if next_token == 'EMPTY': return dict(type='MultiPolygon', coordinates=[]) elif not next_token == '(': raise ValueError(INVALID_WKT_FMT % string) polygons = [] while True: try: poly = _load_polygon(tokens, string) polygons.append(poly['coordinates']) t = next(tokens) if t == ')': # we're done; no more polygons. break except (StopIteration, tokenize.TokenError): # If we reach this, the WKT is not valid. raise ValueError(INVALID_WKT_FMT % string) return dict(type='MultiPolygon', coordinates=polygons) def _load_multilinestring(tokens, string): """ Has similar inputs and return value to to :func:`_load_point`, except is for handling MULTILINESTRING geometry. :returns: A GeoJSON `dict` MultiLineString representation of the WKT ``string``. """ next_token = next(tokens) if next_token == 'EMPTY': return dict(type='MultiLineString', coordinates=[]) elif not next_token == '(': raise ValueError(INVALID_WKT_FMT % string) linestrs = [] while True: try: linestr = _load_linestring(tokens, string) linestrs.append(linestr['coordinates']) t = next(tokens) if t == ')': # we're done; no more linestrings. break except (StopIteration, tokenize.TokenError): # If we reach this, the WKT is not valid. raise ValueError(INVALID_WKT_FMT % string) return dict(type='MultiLineString', coordinates=linestrs) def _load_geometrycollection(tokens, string): """ Has similar inputs and return value to to :func:`_load_point`, except is for handling GEOMETRYCOLLECTIONs. Delegates parsing to the parsers for the individual geometry types. :returns: A GeoJSON `dict` GeometryCollection representation of the WKT ``string``. """ next_token = next(tokens) if next_token == 'EMPTY': return dict(type='GeometryCollection', geometries=[]) elif not next_token == '(': raise ValueError(INVALID_WKT_FMT % string) geoms = [] result = dict(type='GeometryCollection', geometries=geoms) while True: try: t = next(tokens) if t == ')': break elif t == ',': # another geometry still continue else: geom_type = t load_func = _loads_registry.get(geom_type) geom = load_func(tokens, string) geoms.append(geom) except (StopIteration, tokenize.TokenError): raise ValueError(INVALID_WKT_FMT % string) return result _dumps_registry = { 'Point': _dump_point, 'LineString': _dump_linestring, 'Polygon': _dump_polygon, 'MultiPoint': _dump_multipoint, 'MultiLineString': _dump_multilinestring, 'MultiPolygon': _dump_multipolygon, 'GeometryCollection': _dump_geometrycollection, } _loads_registry = { 'POINT': _load_point, 'LINESTRING': _load_linestring, 'POLYGON': _load_polygon, 'MULTIPOINT': _load_multipoint, 'MULTILINESTRING': _load_multilinestring, 'MULTIPOLYGON': _load_multipolygon, 'GEOMETRYCOLLECTION': _load_geometrycollection, } _type_map_caps_to_mixed = dict( POINT='Point', LINESTRING='LineString', POLYGON='Polygon', MULTIPOINT='MultiPoint', MULTILINESTRING='MultiLineString', MULTIPOLYGON='MultiPolygon', GEOMETRYCOLLECTION='GeometryCollection', ) geomet-1.1.0/pyproject.toml000066400000000000000000000026771453626627600157420ustar00rootroot00000000000000[build-system] requires = [ "setuptools >=61", ] build-backend = "setuptools.build_meta" [project] name = "geomet" readme = "README.md" maintainers = [ {name = "Lars Butler", email = "lars.butler@gmail.com"}, ] description = "Pure Python conversion library for common geospatial data formats" requires-python = ">=3.7" keywords = [ "esri", "ewkb", "ewkt", "geojson", "geopackage", "geospatial", "gis", "spatial", "wkb", "wkt", ] license = {text = "Apache-2.0"} classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3.7", "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", "Topic :: Scientific/Engineering :: GIS", ] dependencies = [ "click", ] dynamic = ["version"] [project.scripts] geomet = "geomet.tool:cli" [project.urls] Repository = "https://github.com/geomet/geomet" [tool.setuptools.dynamic] version = {attr = "geomet.__init__.__version__"} [tool.setuptools.packages.find] include = ["geomet", "geomet.*"] exclude = ["geomet.tests", "geomet.tests.*"] geomet-1.1.0/setup.cfg000066400000000000000000000000461453626627600146330ustar00rootroot00000000000000[egg_info] tag_build = tag_date = 0 geomet-1.1.0/setup.py000066400000000000000000000001361453626627600145240ustar00rootroot00000000000000from setuptools import setup # See pyproject.toml for project metadata setup(name='geomet')