├── tests ├── fixtures │ ├── testing.shp │ ├── testing.shx │ ├── testing.dbf │ ├── testing.prj │ ├── testing.qpj │ └── testing.geojson ├── __init__.py ├── test_scale.py ├── test_coords.py ├── test_layer.py ├── test_round.py ├── test_feature.py ├── test_measure.py └── test_geometry.py ├── MANIFEST.in ├── fionautil ├── __init__.py ├── drivers.py ├── coords.py ├── round.py ├── scale.py ├── feature.py ├── geometry.py ├── measure.py └── layer.py ├── tox.ini ├── .coveragerc ├── .travis.yml ├── Makefile ├── .gitignore ├── setup.py └── README.md /tests/fixtures/testing.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitnr/fionautil/master/tests/fixtures/testing.shp -------------------------------------------------------------------------------- /tests/fixtures/testing.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitnr/fionautil/master/tests/fixtures/testing.shx -------------------------------------------------------------------------------- /tests/fixtures/testing.dbf: -------------------------------------------------------------------------------- 1 | _A idN 2 | 3 2 1 4 -------------------------------------------------------------------------------- /tests/fixtures/testing.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /tests/fixtures/testing.qpj: -------------------------------------------------------------------------------- 1 | GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015, Neil Freeman 7 | 8 | recursive-include tests *.py 9 | recursive-include tests/fixtures *.* 10 | -------------------------------------------------------------------------------- /fionautil/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | __version__ = '0.7.0' 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015, Neil Freeman 7 | 8 | [tox] 9 | envlist = py36, py37, py38 10 | 11 | [testenv] 12 | commands = 13 | pip install docutils 14 | make readme.rst 15 | pip install -e .[azimuth,shape] 16 | python setup.py --quiet test 17 | 18 | whitelist_externals = make 19 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015, Neil Freeman 7 | 8 | [run] 9 | omit = 10 | *tests.py 11 | branch = True 12 | source = 13 | fionautil 14 | 15 | [report] 16 | exclude_lines = 17 | pragma: no cover 18 | def __repr__ 19 | raise NotImplementedError 20 | if __name__ == .__main__.: 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | from . import test_coords 12 | from . import test_feature 13 | from . import test_layer 14 | from . import test_measure 15 | from . import test_geometry 16 | from . import test_round 17 | from . import test_scale 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 3.6 5 | - 3.7 6 | - 3.8 7 | 8 | sudo: true 9 | 10 | cache: 11 | directories: 12 | - $HOME/.cache/pip 13 | 14 | env: 15 | - NUMPY=no 16 | - NUMPY=yes 17 | 18 | before_install: 19 | - sudo apt-get -qq update 20 | - sudo apt-get -qq install -y libgdal1-dev 21 | - pip install docutils 22 | - if [[ $NUMPY == "yes" ]]; then pip install numpy; fi 23 | - if [[ $NUMPY == "no" ]]; then pip uninstall -y numpy; fi 24 | 25 | install: 26 | - make 27 | - pip install -e .[azimuth,shape] 28 | 29 | script: 30 | - python setup.py test 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015-6, Neil Freeman 7 | 8 | README.rst: README.md 9 | - pandoc $< -o $@ 10 | @touch $@ 11 | - python setup.py check --restructuredtext --strict 12 | 13 | .PHONY: cov deploy clean 14 | cov: 15 | - coverage run setup.py test 16 | coverage report 17 | coverage html 18 | 19 | deploy: README.rst | clean 20 | python3 setup.py sdist 21 | python3 setup.py bdist_wheel --universal 22 | twine upload dist/* 23 | git push 24 | git push --tags 25 | 26 | clean:; rm -rf dist build fionautil/*.pyc 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015, Neil Freeman 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | bin/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | .eggs 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | readme.rst 64 | 65 | -------------------------------------------------------------------------------- /tests/test_scale.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | from unittest import TestCase as PythonTestCase 12 | import unittest.main 13 | from fionautil import scale 14 | try: 15 | import numpy as np 16 | except ImportError: 17 | pass 18 | 19 | 20 | class TestCoords(PythonTestCase): 21 | 22 | def setUp(self): 23 | self.coords = [(1, 1), (-2, 2), (3, 14), (10, -1)] 24 | self.coordsx2 = [(2, 2), (-4, 4), (6, 28), (20, -2)] 25 | 26 | def test_scale(self): 27 | result = scale.scale(self.coords, 2) 28 | 29 | try: 30 | arr = np.array(self.coordsx2, dtype=float).tolist() 31 | result = result.tolist() 32 | self.assertEqual(result, arr) 33 | 34 | except NameError: 35 | self.assertEqual(list(result), self.coordsx2) 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /fionautil/drivers.py: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015-6, Neil Freeman 7 | 8 | import os.path 9 | from fiona import supported_drivers 10 | 11 | suffix_map = { 12 | 'csv': 'CSV', 13 | 'gdb': 'FileGDB', 14 | 'dbf': 'ESRI Shapefile', 15 | 'shp': 'ESRI Shapefile', 16 | 'gtm': 'GPSTrackMaker', 17 | 'dgn': 'DGN', 18 | 'json': 'GeoJSON', 19 | 'geojson': 'GeoJSON', 20 | 'gpkg': 'GPKG', 21 | 'mapinfo': 'MapInfo File', 22 | 'gpx': 'GPX', 23 | 'dxf': 'DXF', 24 | 'bna': 'BNA', 25 | 'gmt': 'GMT', 26 | } 27 | 28 | 29 | def from_path(path): 30 | _, suffix = os.path.splitext(os.path.basename(path)) 31 | return from_suffix(suffix) 32 | 33 | 34 | def from_suffix(suffix): 35 | '''Attempt to return the name of the appropriate GDAL driver, given a suffix.''' 36 | 37 | if suffix.startswith('.'): 38 | suffix = suffix[1:] 39 | 40 | if suffix in suffix_map and suffix_map[suffix] in supported_drivers: 41 | return suffix_map[suffix] 42 | 43 | return None 44 | -------------------------------------------------------------------------------- /tests/fixtures/testing.geojson: -------------------------------------------------------------------------------- 1 | {"features": [{"geometry": {"coordinates": [[[-91.08315256347775, 48.102544248622834], [-85.20083336953032, 48.055106190607134], [-85.24827142754603, 47.24865920434014], [-91.03571450546204, 47.39097337838725], [-91.08315256347775, 48.102544248622834]]], "type": "Polygon"}, "id": 0, "properties": {"id": 3}, "type": "Feature"}, {"geometry": {"coordinates": [[[-82.02248348247807, 19.023014684995577], [-71.77586295108577, 18.928138568964165], [-71.68098683505433, 10.009783662011598], [-81.88016930843098, 10.009783662011598], [-82.02248348247807, 19.023014684995577]]], "type": "Polygon"}, "id": 1, "properties": {"id": 2}, "type": "Feature"}, {"geometry": {"coordinates": [[[-90.60877198332071, 49.90519045321963], [-86.38678481992295, 49.90519045321963], [-86.38678481992295, 46.062707753947514], [-90.56133392530502, 46.15758386997892], [-90.60877198332071, 49.90519045321963]]], "type": "Polygon"}, "id": 2, "properties": {"id": 1}, "type": "Feature"}, {"geometry": {"coordinates": [[[-8.965792964968273, 5.319703825881174], [7.571114059306538, 5.319703825881174], [7.850049840438885, -12.133706479257063], [-9.125184839901042, -11.974314604324293], [-8.965792964968273, 5.319703825881174]]], "type": "Polygon"}, "id": 3, "properties": {"id": 4}, "type": "Feature"}], "fiona:crs": {"init": "epsg:4326"}, "fiona:schema": {"geometry": "Polygon", "properties": {"id": "int:10"}}, "type": "FeatureCollection"} -------------------------------------------------------------------------------- /fionautil/coords.py: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015-6, Neil Freeman 7 | 8 | from .round import round_ring as roundring 9 | 10 | 11 | def max_x(coords): 12 | return max((c[0] for c in coords)) 13 | 14 | 15 | def max_y(coords): 16 | return max((c[1] for c in coords)) 17 | 18 | 19 | def min_x(coords): 20 | return min((c[0] for c in coords)) 21 | 22 | 23 | def min_y(coords): 24 | return min((c[1] for c in coords)) 25 | 26 | 27 | def segmentize(ring): 28 | for i, point in enumerate(ring[:-1]): 29 | yield point, ring[i + 1] 30 | 31 | 32 | def bounds(ring): 33 | '''Return minimum bounding rectangle for a ring''' 34 | return min_x(ring), min_y(ring), max_x(ring), max_y(ring) 35 | 36 | 37 | def roundpolyring(polyring, precision=None): 38 | return [roundring(ring, precision) for ring in polyring] 39 | 40 | 41 | def centerbounds(bounds): 42 | '''Returns the center of a bounding box.''' 43 | return bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2 44 | 45 | 46 | def cornerbounds(bounds): 47 | '''Returns the four corners of a bounding box''' 48 | return (bounds[0], bounds[1]), (bounds[0], bounds[3]), (bounds[2], bounds[3]), (bounds[2], bounds[1]) 49 | -------------------------------------------------------------------------------- /tests/test_coords.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | from unittest import TestCase as PythonTestCase 12 | import unittest.main 13 | from fionautil import coords 14 | 15 | class TestCoords(PythonTestCase): 16 | 17 | def setUp(self): 18 | self.coords = [(1, 1), (-2, 2), (3, 14), (10, -1)] 19 | 20 | def testMinX(self): 21 | assert coords.min_x(self.coords) == -2 22 | 23 | def testMinY(self): 24 | assert coords.min_y(self.coords) == -1 25 | 26 | def testMaxX(self): 27 | assert coords.max_x(self.coords) == 10 28 | 29 | def testMaxY(self): 30 | assert coords.max_y(self.coords) == 14 31 | 32 | def test_segmentize(self): 33 | segments = coords.segmentize(self.coords) 34 | 35 | self.assertEqual(next(segments), ((1, 1), (-2, 2))) 36 | self.assertEqual(next(segments), ((-2, 2), (3, 14))) 37 | 38 | def test_bounds(self): 39 | assert coords.bounds(self.coords) == (-2, -1, 10, 14) 40 | 41 | def test_cornerbounds(self): 42 | b = (0, 0, 6, 8) 43 | self.assertSequenceEqual(coords.cornerbounds(b), [(0, 0), (0, 8), (6, 8), (6, 0)]) 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /fionautil/round.py: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015-6, Neil Freeman 7 | 8 | try: 9 | import numpy as np 10 | except ImportError: 11 | pass 12 | 13 | 14 | def _round(pt, precision): 15 | try: 16 | return round(pt[0], precision), round(pt[1], precision) 17 | 18 | except TypeError: 19 | pt = list(pt) 20 | return round(pt[0], precision), round(pt[1], precision) 21 | 22 | 23 | def round_ring(ring, precision): 24 | return [_round(tuple(pt), precision) for pt in ring] 25 | 26 | 27 | def geometry(geom, precision): 28 | g = dict(geom.items()) 29 | 30 | if geom['type'] == 'GeometryCollection': 31 | g['geometries'] = [geometry(x, precision) for x in geom['geometries']] 32 | return g 33 | 34 | try: 35 | c = np.array(geom['coordinates']) 36 | g['coordinates'] = np.round(c, precision) 37 | 38 | except (AttributeError, KeyError, TypeError, NameError): 39 | 40 | if geom['type'] == 'Point': 41 | g['coordinates'] = _round(geom['coordinates'], precision) 42 | 43 | elif geom['type'] in ('MultiPoint', 'LineString'): 44 | g['coordinates'] = round_ring(geom['coordinates'], precision) 45 | 46 | elif geom['type'] in ('MultiLineString', 'Polygon'): 47 | g['coordinates'] = [round_ring(r, precision) for r in geom['coordinates']] 48 | 49 | elif geom['type'] == 'MultiPolygon': 50 | g['coordinates'] = [[round_ring(r, precision) for r in rings] for rings in geom['coordinates']] 51 | 52 | return g 53 | 54 | 55 | def feature(feat, precision): 56 | feat['geometry'] = geometry(feat['geometry'], precision) 57 | return feat 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | from setuptools import setup, find_packages 12 | 13 | try: 14 | readme = open('README.rst').read() 15 | except IOError: 16 | readme = '' 17 | 18 | shapely = 'shapely>=1.5.0,<2.0' 19 | 20 | with open('fionautil/__init__.py') as i: 21 | version = next(r for r in i.readlines() if '__version__' in r).split('=')[1].strip('"\' \n') 22 | 23 | setup( 24 | name='fionautil', 25 | version=version, 26 | description='helpful utilities for working with geodata with Fiona', 27 | long_description=readme, 28 | keywords='GIS', 29 | author='Neil Freeman', 30 | author_email='contact@fakeisthenewreal.org', 31 | url='http://github.com/fitnr/fionautil/', 32 | license='GNU General Public License v3 (GPLv3)', 33 | classifiers=[ 34 | 'Development Status :: 4 - Beta', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7', 39 | 'Programming Language :: Python :: 3.8', 40 | 'Operating System :: OS Independent', 41 | ], 42 | packages=find_packages(), 43 | include_package_data=True, 44 | install_requires=[ 45 | 'fiona>=1.7.1,<2.0', 46 | ], 47 | 48 | extras_require={ 49 | 'shapify': [shapely], 50 | 'shape': [shapely], 51 | 'length': [shapely], 52 | 'dissolve': [shapely], 53 | 'speed': ['numpy>1.9'], 54 | 'azimuth': ['pyproj>=1.9.5,<1.10'], 55 | }, 56 | 57 | test_suite='tests', 58 | ) 59 | -------------------------------------------------------------------------------- /fionautil/scale.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Scale geometries and features.""" 4 | 5 | # This file is part of fionautil. 6 | # http://github.com/fitnr/fionautil 7 | 8 | # Licensed under the GPLv3 license: 9 | # http://http://opensource.org/licenses/GPL-3.0 10 | # Copyright (c) 2015-6, Neil Freeman 11 | 12 | try: 13 | import numpy as np 14 | except ImportError: 15 | pass 16 | 17 | 18 | def geometry(geom, factor=1): 19 | g = dict(geom.items()) 20 | 21 | if geom['type'] == 'MultiPolygon': 22 | g['coordinates'] = [scale_rings(rings, factor) for rings in geom['coordinates']] 23 | 24 | elif geom['type'] in ('Polygon', 'MultiLineString'): 25 | g['coordinates'] = scale_rings(geom['coordinates'], factor) 26 | 27 | elif geom['type'] in ('MultiPoint', 'LineString'): 28 | g['coordinates'] = scale(geom['coordinates'], factor) 29 | 30 | elif geom['type'] == 'Point': 31 | g['coordinates'] = scale(geom['coordinates'], factor) 32 | 33 | elif geom['type'] == 'GeometryCollection': 34 | g['geometries'] = [geometry(i) for i in geom['geometries']] 35 | 36 | else: 37 | raise NotImplementedError("Unknown geometry type") 38 | 39 | return g 40 | 41 | 42 | def scale_rings(rings, factor=1): 43 | return [scale(ring, factor) for ring in rings] 44 | 45 | 46 | def scale(coordinates, scalar=1): 47 | '''Scale a list of coordinates by a scalar. Only use with projected coordinates''' 48 | try: 49 | try: 50 | arr = np.array(coordinates, dtype=float) 51 | 52 | except TypeError: 53 | arr = np.array(list(coordinates), dtype=float) 54 | 55 | return arr * scalar 56 | 57 | except NameError: 58 | if isinstance(coordinates, tuple): 59 | return [coordinates[0] * scalar, coordinates[1] * scalar] 60 | 61 | return [(c[0] * scalar, c[1] * scalar) for c in coordinates] 62 | 63 | 64 | def feature(feat, factor=1): 65 | return { 66 | 'properties': feat.get('properties'), 67 | 'geometry': geometry(feat['geometry'], factor), 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### fionautil 2 | 3 | Utilities for working with geodata with [Fiona](https://pypi.python.org/pypi/Fiona/1.5.0). 4 | 5 | By default, the only prerequisite is Fiona itself. 6 | 7 | By default, the package installs without shapely. A small number of functions, marked below, do require shapely. To use these function, install with `pip install fionautil[functionname]` or just separately install shapely. 8 | 9 | ## Contents 10 | 11 | ### drivers 12 | 13 | Tools for fetching the driver name, given a file suffix 14 | 15 | * from_file 16 | * From suffix 17 | 18 | ### feature 19 | 20 | * field_contains_test (test if a feature's properties has certain key:value pairs) 21 | * togeojson (return a geojson-ready object) 22 | * shapify (requires shapely) 23 | * length (requires shapely) 24 | * compound 25 | 26 | ### geometry 27 | 28 | * endpoints (for polyline features) 29 | * startpoint (for polyline features) 30 | * endpoint (for polyline features) 31 | * azimuth (between the start and end of a polyline) 32 | * disjointed 33 | * explodepoints 34 | * explodesegments 35 | * exploderings 36 | * countpoints 37 | * countsegments 38 | * roundgeometry - round all coordinates in a geometry to a given precision 39 | 40 | ### layer 41 | 42 | Most of these tools mimic builtin python itertools. 43 | 44 | * ffilter 45 | * ffilterfalse 46 | * fmap 47 | * fchain 48 | * freduce 49 | * fslice 50 | * fzip 51 | * length Total length of linear features in a file's native projection or the given Proj object 52 | * meta (returns a layer's meta attribute) 53 | * meta_complete (returns the meta attribute with addional metadata, e.g. bounds) 54 | * bounds (returns a layer's bounds) 55 | * find (return a feature that matches a particular key=value) 56 | 57 | ### measure 58 | 59 | * distance (between two coordinates) 60 | * azimuth (between two coordinates) 61 | * signed_area 62 | * clockwise (shortcut for checking if signed_area is >= 0) 63 | * counterclockwise (shortcut for checking if signed_area is < 0) 64 | * azimuth_distance (returns both azimuth and distance between two points) 65 | * intersect (check if two planar line segments intersect) 66 | * onsegment (check if a point lines on a line segment) 67 | * intersectingbounds (check if two bounding boxes intersect) 68 | 69 | ### round 70 | * geometry - round all coordinates in a geometry to a specified precision 71 | * feature 72 | 73 | ### scale 74 | 75 | Utilities for scaling a feature or geometry by a given constant. Goes faster with Numpy installed. 76 | 77 | * geometry 78 | * scale_rings 79 | * scale - scales a list of coordinates 80 | * feature - scale the geometry of a feature 81 | -------------------------------------------------------------------------------- /fionautil/feature.py: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015-6, Neil Freeman 7 | 8 | try: 9 | from shapely.geometry import shape as shapelyshape 10 | except ImportError: 11 | pass 12 | 13 | 14 | def field_contains_test(field_values): 15 | ''' 16 | Return a test function that checks if the properties of a feature match the possible values in field_values. 17 | 18 | >>> test = field_contains_test({'a': (1, 2, 3)}) 19 | >>> test({'properties': {'a': 1}}) 20 | True 21 | >>> test({'properties': {'a': 4}}) 22 | False 23 | ''' 24 | def test(feature): 25 | for k, v in field_values.items(): 26 | if feature['properties'].get(k) not in v: 27 | return False 28 | return True 29 | 30 | return test 31 | 32 | 33 | def togeojson(typ, coordinates, properties=None): 34 | '''Return a GeoJSON-ready object given a properties dict, a type and coordinates.''' 35 | properties = properties or {} 36 | 37 | return { 38 | 'type': 'Feature', 39 | 'properties': properties, 40 | 'geometry': { 41 | 'type': typ, 42 | 'coordinates': coordinates, 43 | } 44 | } 45 | 46 | 47 | def shape(feature): 48 | '''Applies shapely.geometry.shape to the geometry part of a feature 49 | and returns a new feature object with properties intact''' 50 | try: 51 | return { 52 | 'properties': feature.get('properties'), 53 | 'geometry': shapelyshape(feature['geometry']) 54 | } 55 | 56 | except NameError: 57 | raise NotImplementedError("shapify requires shapely") 58 | 59 | 60 | def shapify(feature): 61 | '''Applies shapely.geometry.shape to the geometry part of a feature 62 | and returns a new feature object with properties intact''' 63 | return shape(feature) 64 | 65 | 66 | def length(feature): 67 | '''Returns shapely length''' 68 | try: 69 | geom = shapelyshape(feature['geometry']) 70 | return geom.length 71 | 72 | except NameError: 73 | raise NotImplementedError("length requires shapely") 74 | 75 | 76 | def compound(feature): 77 | '''Returns True if feature has more than one part: 78 | * A polygon with holes 79 | * A MultiPolygon or MultiLineString 80 | ''' 81 | if 'Multi' in feature['geometry']['type']: 82 | return True 83 | 84 | if (feature['geometry']['type'] == 'Polygon' and 85 | len(feature['geometry']['coordinates']) > 1): 86 | return True 87 | 88 | return False 89 | 90 | 91 | def area(feature): 92 | try: 93 | geom = shapelyshape(feature['geometry']) 94 | return geom.area 95 | 96 | except NameError: 97 | raise NotImplementedError("length requires shapely") 98 | -------------------------------------------------------------------------------- /tests/test_layer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | import collections 12 | from unittest import TestCase as PythonTestCase 13 | import unittest.main 14 | import os.path 15 | import fionautil.layer 16 | 17 | shp = os.path.join(os.path.dirname(__file__), 'fixtures/testing.shp') 18 | geojson = os.path.join(os.path.dirname(__file__), 'fixtures/testing.geojson') 19 | 20 | 21 | class TestLayer(PythonTestCase): 22 | 23 | def test_ffilter(self): 24 | test = lambda f: f['properties']['id'] == 3 25 | assert len(list(fionautil.layer.ffilter(test, shp))) == 1 26 | assert len(list(fionautil.layer.ffilter(test, geojson))) == 1 27 | 28 | def test_ffilterfalse(self): 29 | test = lambda f: f['properties']['id'] == 3 30 | assert len(list(fionautil.layer.ffilterfalse(test, shp))) == 3 31 | assert len(list(fionautil.layer.ffilterfalse(test, shp))) == 3 32 | 33 | def test_fiter(self): 34 | s = fionautil.layer.fiter(shp) 35 | d = collections.deque(s, maxlen=0) 36 | assert len(d) == 0 37 | assert next(fionautil.layer.fiter(geojson)) 38 | 39 | def test_fmap(self): 40 | def func(f): 41 | f['properties']['cat'] = 'meow' 42 | return f 43 | 44 | assert next(fionautil.layer.fmap(func, shp)).get('properties').get('cat') == 'meow' 45 | assert next(fionautil.layer.fmap(func, geojson)).get('properties').get('cat') == 'meow' 46 | 47 | def test_freduce(self): 48 | def func(g, f): 49 | g = g or {'geometry': {'coordinates': []}} 50 | g['geometry']['coordinates'] = g['geometry']['coordinates'] + f['geometry']['coordinates'] 51 | return g 52 | 53 | start = { 54 | 'geometry': { 55 | 'coordinates': [] 56 | } 57 | } 58 | 59 | assert fionautil.layer.freduce(func, shp, start) 60 | assert fionautil.layer.freduce(func, geojson) 61 | assert len(fionautil.layer.freduce(func, shp, start).get('geometry').get('coordinates')) == 8 62 | 63 | def test_fchain(self): 64 | chain = fionautil.layer.fchain(shp, geojson) 65 | assert len(list(chain)) == 8 66 | 67 | def test_fslice(self): 68 | assert list(fionautil.layer.fslice(shp, 1, 2)) 69 | assert list(fionautil.layer.fslice(geojson, 0, 4, 2)) 70 | 71 | def test_fzip(self): 72 | assert list(fionautil.layer.fzip(shp, geojson)) 73 | assert len(list(fionautil.layer.fzip(shp, geojson))) == 4 74 | assert type(next(fionautil.layer.fzip(shp, geojson))) == tuple 75 | 76 | def test_find(self): 77 | two = fionautil.layer.find(shp, 'id', 2) 78 | one = fionautil.layer.find(geojson, 'id', 1) 79 | assert one['properties']['id'] == 1 80 | assert two['properties']['id'] == 2 81 | 82 | def test_layer_args(self): 83 | def func(f, n): 84 | f['properties']['n'] = n 85 | return f 86 | 87 | assert next(fionautil.layer.fmap(func, shp, n='meow')).get('properties').get('n') == 'meow' 88 | assert next(fionautil.layer.fmap(func, geojson, n='meow')).get('properties').get('n') == 'meow' 89 | 90 | 91 | if __name__ == '__main__': 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /tests/test_round.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | import unittest 12 | import fionautil.round 13 | try: 14 | import numpy as np 15 | except ImportError: 16 | pass 17 | 18 | 19 | class RoundTestCase(unittest.TestCase): 20 | 21 | def setUp(self): 22 | self.polygon = { 23 | "type": "Polygon", 24 | "coordinates": [ 25 | [ 26 | [100.11111, 0.0], 27 | [101.0, 0.11111], 28 | [101.0, 1.11111], 29 | [100.11111, 1.0], 30 | [100.11111, 0.0] 31 | ] 32 | ] 33 | } 34 | 35 | def testRound(self): 36 | ring = [(10.00011111, 10.00011111), (10.00011111, 10.00011111)] 37 | round1 = fionautil.round._round(ring[0], 1) 38 | round2 = fionautil.round.round_ring(ring, 1) 39 | 40 | try: 41 | round1 = round1.tolist() 42 | round2 = round2[0].tolist() 43 | except AttributeError: 44 | round2 = round2[0] 45 | 46 | self.assertSequenceEqual(round1, (10.0, 10.0)) 47 | self.assertSequenceEqual(round2, (10.0, 10.0)) 48 | 49 | def testRoundPolygon(self): 50 | g = fionautil.round.geometry(self.polygon, 3) 51 | 52 | try: 53 | coordinates = g['coordinates'].tolist() 54 | except AttributeError: 55 | coordinates = g['coordinates'] 56 | 57 | self.assertSequenceEqual(coordinates[0][0], [100.111, 0.0]) 58 | 59 | def testRoundMultiPolygon(self): 60 | mp = { 61 | "type": "MultiPolygon", 62 | "coordinates": [self.polygon['coordinates'], self.polygon['coordinates']] 63 | } 64 | g = fionautil.round.geometry(mp, 3) 65 | 66 | try: 67 | coordinates = g['coordinates'][0][0][0].tolist() 68 | except AttributeError: 69 | coordinates = g['coordinates'][0][0][0] 70 | 71 | self.assertSequenceEqual(coordinates, [100.111, 0.0]) 72 | 73 | def testRoundFeature(self): 74 | feat = { 75 | "geometry": self.polygon, 76 | "properties": {"foo": "bar"} 77 | } 78 | f = fionautil.round.feature(feat, 4) 79 | 80 | try: 81 | coordinates = f['geometry']['coordinates'][0][0].tolist() 82 | except AttributeError: 83 | coordinates = f['geometry']['coordinates'][0][0] 84 | 85 | self.assertSequenceEqual(coordinates, [100.1111, 0.0]) 86 | assert f['properties']['foo'] == 'bar' 87 | 88 | def testRoundGenerator(self): 89 | x = (float(x) + 0.555 for x in range(2)) 90 | b = fionautil.round._round(x, 2) 91 | 92 | try: 93 | b = b.tolist() 94 | except AttributeError: 95 | pass 96 | 97 | self.assertSequenceEqual(b, (0.56, 1.56)) 98 | 99 | def testRoundListIfNumpy(self): 100 | try: 101 | np 102 | except NameError: 103 | return 104 | 105 | result = fionautil.round.geometry(self.polygon, 0) 106 | 107 | self.assertSequenceEqual(result['coordinates'][0][0].tolist(), [100.0, 0.0]) 108 | 109 | if __name__ == '__main__': 110 | unittest.main() 111 | -------------------------------------------------------------------------------- /tests/test_feature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | from unittest import TestCase as PythonTestCase 12 | import unittest.main 13 | import os.path 14 | from fionautil import feature 15 | 16 | shp = os.path.join(os.path.dirname(__file__), 'fixtures/testing.shp') 17 | geojson = os.path.join(os.path.dirname(__file__), 'fixtures/testing.geojson') 18 | 19 | 20 | class TestFeature(PythonTestCase): 21 | 22 | def setUp(self): 23 | self.polygon = {'properties': {'a': 1, 'b': 2}, 'geometry': {'type': 'Polygon', 'coordinates': [[(-122.002301, 37.529835999999996), (-122.002236, 37.529908), (-122.001506, 37.529309999999995), (-122.002196, 37.529679), (-122.00236, 37.529773999999996), (-122.002301, 37.529835999999996)], [(-156.71787267699898, 21.137419760764438), (-156.76604819764898, 21.06517681773707), (-156.8864840998058, 21.04913411712499), (-157.07113192831787, 21.105330956858555), (-156.71787267699898, 21.137419760764438)]]}} 24 | 25 | self.linestring = {'geometry': {'type': 'LineString', 'coordinates': [(1698558.6560016416, 785359.5722958631), (1698570.6255255828, 785357.1199829046), (1698592.7188758815, 785353.618717982), (1698615.888545638, 785342.774320389), (1698632.1053597904, 785336.6905814429), (1698651.8725316962, 785330.0421180109), (1698659.045153662, 785327.3537529241), (1698688.1001785155, 785316.4641486479), (1698696.6319864516, 785313.4574198754), (1698717.936275069, 785303.1641204398), (1698733.601512783, 785295.2805483269), (1698757.090320928, 785281.0787615285), (1698773.2214488257, 785270.9157159382), (1698802.13278612, 785251.4878775944), (1698839.0495838472, 785227.758052662), (1701103.0715925542, 785570.0575142633), (1701117.0415831083, 785569.3192712615), (1701143.1992729052, 785566.1717272209), (1701168.650131336, 785560.3315072984), (1701209.706061085, 785548.4685232819), (1701250.512021864, 785532.6336759274), (1701281.5801281473, 785515.853762881), (1701310.5929042343, 785499.6991391898), (1701378.6116007813, 785459.892916804), (1701445.1270219584, 785422.3276661132), (1701482.1400952877, 785402.5158508102), (1701506.6506188242, 785391.8440056049), (1701523.390860305, 785385.4644141722), (1701559.0541053142, 785373.9359225394), (1701572.477673862, 785369.6783910012)]}} 26 | 27 | self.multipolygon = {'geometry': {'type': 'MultiPolygon', 'coordinates': [[[(-156.71787267699898, 21.137419760764438), (-156.76604819764898, 21.06517681773707), (-156.8864840998058, 21.04913411712499), (-157.07113192831787, 21.105330956858555), (-157.28789752278652, 21.081250571840183), (-157.30394879695828, 21.137448432663767), (-157.2477475061229, 21.161530451508398), (-157.23169415834994, 21.233776323139818), (-157.16747148788883, 21.19364045634365), (-157.00690374246722, 21.18561063798249), (-156.95873348755612, 21.209693513430512), (-156.94268251253186, 21.161526867134565), (-156.71787267699898, 21.137419760764438)]], [[(-156.1960454124824, 20.631649407365213), (-156.27631740739284, 20.583483860915248), (-156.3967349429455, 20.567426981472988), (-156.43687923087785, 20.623621217336662), (-156.46097189319752, 20.727981087256364), (-156.49307474018514, 20.792204281510333), (-156.5251935717042, 20.776149657497466), (-156.63758660416465, 20.80826091204387), (-156.69378263402297, 20.912624010061208), (-156.65363600731771, 21.016985049562912), (-156.5974396197875, 21.041064705415824), (-156.5251916458635, 20.98487016377238), (-156.47702205254387, 20.89656513911172), (-156.3566035032919, 20.9447263610079), (-156.26026336307226, 20.928671268747912), (-156.01139245254785, 20.800225321607478), (-155.98731705257018, 20.752061631687628), (-156.0435115425545, 20.65573259996677), (-156.1318089039728, 20.62362291867985), (-156.1960454124824, 20.631649407365213)]], [[(-157.03905067093797, 20.928706972385005), (-156.91058584516276, 20.928718617694337), (-156.80620534154883, 20.84041861217258), (-156.81422569781648, 20.7922527172797), (-156.88648717839553, 20.73604940916775), (-156.96676559866026, 20.728020731638775), (-156.9908634657756, 20.792237151462064), (-156.9828263229743, 20.832377626807837), (-157.05509821197901, 20.880538425907034), (-157.03905067093797, 20.928706972385005)]]]}} 28 | 29 | 30 | def testCompound(self): 31 | assert feature.compound(self.linestring) == False 32 | assert feature.compound(self.multipolygon) == True 33 | assert feature.compound(self.polygon) == True 34 | 35 | def testFieldContains(self): 36 | test1 = feature.field_contains_test({'a': (1, 2, 3)}) 37 | test2 = feature.field_contains_test({'b': (7, 8, 9)}) 38 | 39 | assert test1(self.polygon) == True 40 | assert test2(self.polygon) == False 41 | 42 | if __name__ == '__main__': 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/test_measure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of fionautil. 5 | # http://github.com/fitnr/fionautil 6 | 7 | # Licensed under the GPLv3 license: 8 | # http://http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | 11 | from unittest import TestCase as PythonTestCase 12 | import unittest.main 13 | from functools import partial 14 | from math import pi 15 | from os import path 16 | from fionautil import measure 17 | import fionautil.layer 18 | 19 | shp = path.join(path.dirname(__file__), 'fixtures/testing.shp') 20 | 21 | 22 | class TestMeasure(PythonTestCase): 23 | 24 | def testDistance(self): 25 | assert measure.distance(1, 0, 0, 0, False) == 1 26 | self.assertEqual(measure.distance(0, 0, 360, 0, True), 0.0) 27 | self.assertEqual(measure.distance(0, 0, 6, 0, True), 667916.9447596414) 28 | 29 | def testAzimuth(self): 30 | self.assertEqual(measure.azimuth(0, 0, 0, 0, clockwise=True, longlat=False), 0) 31 | self.assertEqual(measure.azimuth(1, 0, 0, 1, clockwise=True, longlat=False), -45) 32 | self.assertEqual(measure.azimuth(0, 1, 1, 0, clockwise=True, longlat=False), 135) 33 | self.assertEqual(measure.azimuth(0, 0, 0, 1, clockwise=True, longlat=False), 0) 34 | self.assertEqual(measure.azimuth(0, 0, 1, 0, clockwise=False, longlat=False), -90) 35 | self.assertEqual(measure.azimuth(1, 0, 0, 0, clockwise=True, longlat=False), 270) 36 | 37 | self.assertEqual(measure.azimuth(0, 0, 0, 90), -0.0) 38 | self.assertEqual(measure.azimuth(0, 0, 90, 0), -90.0) 39 | self.assertEqual(measure.azimuth(0, 0, 90, 0, radians=True), pi / -2) 40 | 41 | def testSignedArea(self): 42 | feature = fionautil.layer.first(shp) 43 | self.assertEqual(measure.signed_area(feature['geometry']['coordinates'][0]), -4.428726877457176) 44 | 45 | coords = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)] 46 | assert measure.signed_area(coords) == 1.0 47 | 48 | assert measure.clockwise(coords) == False 49 | assert measure.counterclockwise(coords) == True 50 | 51 | coords.reverse() 52 | 53 | assert measure.signed_area(coords) == -1.0 54 | assert measure.clockwise(coords) == True 55 | assert measure.counterclockwise(coords) == False 56 | 57 | zcoords = [(0, 0, 1), (1, 0, 0), (1, 1, 0), (0, 1, 0), (0, 0, 0)] 58 | assert measure.signed_area(zcoords) == 1.0 59 | 60 | def testAzimuthDistance(self): 61 | self.assertEqual(measure.azimuth_distance(0, 0, 90, 0), (90, 10018754.171394622)) 62 | self.assertEqual(measure.azimuth_distance(1, 0, 0, 0, longlat=False), (-270, 1)) 63 | 64 | def testDet(self): 65 | assert measure.det((1, 2), (3, 4)) == -2 66 | assert measure.det((3, 4), (1, 2)) == 2 67 | assert measure.det((100, 4), (1, -100)) == -10004 68 | 69 | def testIntersectingbounds(self): 70 | a = (0, 0, 10, 10) 71 | b = (0, 0, 9, 9) 72 | c = (10, 10, 20, 20) 73 | 74 | assert measure.intersectingbounds(a, b) is True 75 | assert measure.intersectingbounds(a, c) is True 76 | assert measure.intersectingbounds(b, c) is False 77 | 78 | def testIntersection(self): 79 | a = ((0, 0), (10, 10)) 80 | b = ((10, 0), (0, 10)) 81 | c = ((0, 5), (10, 5)) 82 | d = ((12, 100), (13, 102)) 83 | e = (0, 0), (5, 5) 84 | 85 | f = ((0, 0), (10, 0)) 86 | g = ((5, 0), (15, 0)) 87 | 88 | h = ((0, 0), (0, 10)) 89 | i = ((0, 5), (0, 15)) 90 | 91 | j = ((11, 11), (12, 12)) 92 | 93 | k = (4, 5), (10, 11) 94 | m = (10, 10), (0, 0) 95 | n = (0, 10), (10, 10) 96 | 97 | self.assertEqual(measure.intersect(a, b), (5, 5)) 98 | self.assertEqual(measure.intersect(a, c), (5, 5)) 99 | 100 | self.assertIn(measure.intersect(a, e), list(a) + list(e)) 101 | assert measure.intersect(e, a) in list(a) + list(e) 102 | 103 | assert measure.intersect(a, d) is None 104 | assert measure.intersect(b, d) is None 105 | 106 | assert measure.intersect(f, g) in list(f) + list(g) 107 | assert measure.intersect(g, f) in list(f) + list(g) 108 | 109 | assert measure.intersect(h, i) in list(h) + list(i) 110 | assert measure.intersect(i, h) in list(h) + list(i) 111 | 112 | assert measure.intersect(a, j) is None 113 | assert measure.intersect(k, m) is None 114 | assert measure.intersect(k, n) == (9, 10) 115 | 116 | def testIntersectionDet(self): 117 | minx, miny, maxx, maxy = 0, 0, 10, 10 118 | edges = ( 119 | ((minx, miny), (minx, maxy)), 120 | ((minx, maxy), (maxx, maxy)), 121 | ((maxx, maxy), (maxx, miny)), 122 | ((maxx, miny), (minx, miny)) 123 | ) 124 | dets = [measure.det(*j) for j in edges] 125 | assert dets == [0, -100, -100, 0] 126 | 127 | a = (4, 5), (10, 11) 128 | assert measure.intersect(edges[0], a) is None 129 | assert measure.intersect(edges[1], a) == (9, 10) 130 | 131 | inters = [measure.intersect(e, a, detm=d) for e, d in zip(edges, dets)] 132 | self.assertListEqual(inters, [None, (9, 10), None, None]) 133 | 134 | def testBoundsIntersect(self): 135 | intersect = partial(measure.intersectingbounds, (0, 0, 1, 1)) 136 | 137 | assert intersect((0.5, 0.5, 1.5, 1.5)) is True 138 | assert intersect((-1, -1, 0.5, 0.5)) is True 139 | assert intersect((0, 0, 1, 1)) is True 140 | assert intersect((0, -1, 0, 1)) is True 141 | assert intersect((0.25, 1.25, 0.75, 1.75)) is False 142 | assert intersect((0.25, 0.25, 0.75, 0.75)) is True 143 | assert intersect((0.25, 0.25, 0.75, 4)) is True 144 | 145 | if __name__ == '__main__': 146 | unittest.main() 147 | -------------------------------------------------------------------------------- /fionautil/geometry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of fionautil. 3 | # http://github.com/fitnr/fionautil 4 | 5 | # Licensed under the GPLv3 license: 6 | # http://http://opensource.org/licenses/GPL-3.0 7 | # Copyright (c) 2015-6, Neil Freeman 8 | 9 | from . import coords 10 | from . import measure 11 | from .round import geometry as roundgeometry 12 | 13 | __all__ = [ 14 | 'endpoints', 15 | 'startpoint', 16 | 'endpoint', 17 | 'azimuth', 18 | 'disjointed', 19 | 'explodepoints', 20 | 'explodesegments', 21 | 'exploderings', 22 | 'countpoints', 23 | 'countsegments', 24 | 'roundgeometry' 25 | ] 26 | 27 | 28 | def endpoints(geometry): 29 | '''Return the first and last coordinates of a LineString or Multilinestring''' 30 | coords = geometry['coordinates'] 31 | 32 | if geometry['type'] == 'MultiLineString': 33 | return coords[0][0], coords[-1][-1] 34 | 35 | elif geometry['type'] == 'LineString': 36 | return coords[0], coords[-1] 37 | 38 | raise ValueError("Improper Feature type.") 39 | 40 | 41 | def startpoint(geometry): 42 | '''Return the first point in a linear geometry''' 43 | start, _ = endpoints(geometry) 44 | return start 45 | 46 | 47 | def endpoint(geometry): 48 | '''Return the last point in a linear geometry''' 49 | _, end = endpoints(geometry) 50 | return end 51 | 52 | 53 | def azimuth(geometry, crs=None, radians=None, clockwise=None, longlat=None): 54 | ''' 55 | Get the azimuth between the start and end points of a polyline geometry. 56 | 57 | Args: 58 | geometry (dict): A geojson-like geometry object 59 | crs (dict): A fiona-style mapping 60 | radians (bool): Output in radians 61 | clockwise (bool): Use clockwise orientation. 62 | longlat (bool): geometry's coordinates are in long-lat form. 63 | 64 | Returns: 65 | (float) The angle (from north) described when one is standing at 66 | the start of the feature and pointing to the end. 67 | ''' 68 | 69 | if geometry['type'] not in ('LineString', 'MultiLineString'): 70 | raise ValueError("This only works with PolyLine layers, this is a {type}".format(**geometry)) 71 | 72 | crs = '' or crs 73 | if longlat is None and 'proj' in crs: 74 | longlat = crs['proj'] == 'longlat' 75 | 76 | first, last = endpoints(geometry) 77 | 78 | points = first + last 79 | 80 | return measure.azimuth(*points, radians=radians, clockwise=clockwise, longlat=longlat) 81 | 82 | 83 | def disjointed(shapes): 84 | '''Reduce a list of shapely shapes to those that are disjoint from the others''' 85 | newshapes = [shapes[0]] 86 | for shape in shapes[1:]: 87 | for i, other in enumerate(newshapes): 88 | if shape.intersects(other): 89 | newshapes[i] = other.union(shape) 90 | break 91 | 92 | return newshapes 93 | 94 | 95 | def explodepoints(geometry): 96 | '''Generator that returns every coordinate a geometry''' 97 | if geometry['type'] == 'Point': 98 | yield geometry['coordinates'] 99 | 100 | else: 101 | for ring in exploderings(geometry): 102 | for point in ring: 103 | yield point 104 | 105 | 106 | def explodesegments(geometry): 107 | '''Generator that returns every line segment of a geometry''' 108 | # Sure, could just use explodepoints, but isn't that a 109 | # bit more memory-intensive, copying all those lists of coords? 110 | if geometry['type'] in ('MultiLineString', 'Polygon'): 111 | for ring in geometry['coordinates']: 112 | for i, point in enumerate(ring[:-1]): 113 | yield point, ring[i + 1] 114 | 115 | elif geometry['type'] in ('LineString', 'MultiPoint'): 116 | for i, point in enumerate(geometry['coordinates'][:-1]): 117 | yield point, geometry['coordinates'][i + 1] 118 | 119 | elif geometry['type'] == 'MultiPolygon': 120 | for poly in geometry['coordinates']: 121 | for ring in poly: 122 | for i, point in enumerate(ring[:-1]): 123 | yield point, ring[i + 1] 124 | 125 | elif geometry['type'] == 'GeometryCollection': 126 | for g in geometry['geometries']: 127 | for i, j in explodesegments(g): 128 | yield i, j 129 | 130 | else: 131 | raise ValueError("Unknown or invalid geometry type: {type}".format(**geometry)) 132 | 133 | 134 | def exploderings(geometry): 135 | '''Generator that returns every ring of a geometry (a ring is a list of points).''' 136 | if geometry['type'] in ('MultiLineString', 'Polygon'): 137 | for ring in geometry['coordinates']: 138 | yield ring 139 | 140 | elif geometry['type'] in ('LineString', 'MultiPoint'): 141 | yield geometry['coordinates'] 142 | 143 | elif geometry['type'] == 'MultiPolygon': 144 | for poly in geometry['coordinates']: 145 | for ring in poly: 146 | yield ring 147 | 148 | elif geometry['type'] == 'GeometryCollection': 149 | for g in geometry['geometries']: 150 | for ring in exploderings(g): 151 | yield ring 152 | 153 | else: 154 | raise ValueError("Unknown or invalid geometry type: {type}".format(**geometry)) 155 | 156 | 157 | def countpoints(geometry): 158 | '''Returns the number of points in a geometry''' 159 | if geometry['type'] == 'Point': 160 | return 1 161 | 162 | else: 163 | return sum(len(ring) for ring in exploderings(geometry)) 164 | 165 | 166 | def countsegments(geometry): 167 | '''Not guaranteed for (multi)point layers''' 168 | if geometry['type'] == 'Point': 169 | return 0 170 | 171 | else: 172 | return sum(len(ring) - 1 for ring in exploderings(geometry)) 173 | 174 | 175 | def bounds(geometry): 176 | a, b, c, d = list(zip(*[coords.bounds(ring) for ring in exploderings(geometry)])) 177 | return min(a), min(b), max(c), max(d) 178 | -------------------------------------------------------------------------------- /fionautil/measure.py: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015-6, Neil Freeman 7 | 8 | import math 9 | 10 | try: 11 | import pyproj 12 | WGS84 = pyproj.Geod(ellps='WGS84') 13 | except ImportError: 14 | pass 15 | 16 | 17 | def distance(x0, y0, x1, y1, longlat=True): 18 | '''distance (in m) between two pairs of points''' 19 | if longlat: 20 | try: 21 | _, _, d = WGS84.inv(x0, y0, x1, y1) 22 | except NameError: 23 | raise NotImplementedError("Distance of long/lat coordinates requires pyproj.") 24 | else: 25 | d = math.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2) 26 | return d 27 | 28 | 29 | def distance_points(p0, p1, longlat=True): 30 | '''distance (in m) between two points''' 31 | return distance(p0[0], p0[1], p1[0], p1[1], longlat) 32 | 33 | 34 | def _projected_azimuth(x0, y0, x1, y1): 35 | '''The angle of a line between two points on a cartesian plane. Always clockwise and in degrees''' 36 | if y0 == y1: 37 | if x0 == x1: 38 | az = 0. 39 | elif x1 > x0: 40 | az = math.pi / 2. 41 | else: 42 | az = 3 * math.pi / 2. 43 | else: 44 | az = math.atan2((x1 - x0), (y1 - y0)) 45 | 46 | return math.degrees(az) 47 | 48 | 49 | def azimuth(x0, y0, x1, y1, radians=False, clockwise=None, longlat=True): 50 | ''' 51 | Measure the azimuth between 2 geographic points 52 | 53 | Args: 54 | x0 (float): first x coordinate. 55 | y0 (float): first y coordinate. 56 | x1 (float): second x coordinate. 57 | y1 (float): second y coordinate. 58 | radians (bool): Return in radians. 59 | clockwise (bool): Return with clockwise coordinates. 60 | longlat (bool): Input is in longitude-latitude, rather than projected. 61 | 62 | Returns: 63 | float 64 | ''' 65 | if longlat: 66 | # this is always in angles 67 | try: 68 | az, _, _ = WGS84.inv(x0, y0, x1, y1) 69 | except NameError: 70 | raise NotImplementedError("Distance of long/lat coordinates requires pyproj.") 71 | 72 | else: 73 | az = _projected_azimuth(x0, y0, x1, y1) 74 | 75 | if radians: 76 | az = math.radians(az) 77 | 78 | return az * (1 if clockwise else -1) 79 | 80 | 81 | def azimuth_points(p0, p1, radians=False, clockwise=None, longlat=True): 82 | return azimuth(p0[0], p0[1], p1[0], p1[1], radians, clockwise, longlat) 83 | 84 | 85 | def signed_area(coords): 86 | """Return the signed area enclosed by a ring using the linear time 87 | algorithm at http://www.cgafaq.info/wiki/Polygon_Area. A value >= 0 88 | indicates a counter-clockwise oriented ring.""" 89 | 90 | try: 91 | xs, ys = tuple(map(list, zip(*coords))) 92 | except ValueError: 93 | # Attempt to handle a z-dimension 94 | xs, ys, _ = tuple(map(list, zip(*coords))) 95 | 96 | xs.append(xs[1]) 97 | ys.append(ys[1]) 98 | return sum(xs[i] * (ys[i + 1] - ys[i - 1]) for i in range(1, len(coords))) / 2. 99 | 100 | 101 | def clockwise(coords): 102 | return signed_area(coords) < 0 103 | 104 | 105 | def counterclockwise(coords): 106 | return signed_area(coords) >= 0 107 | 108 | 109 | def azimuth_distance(x0, y0, x1, y1, longlat=True): 110 | '''Azimuth and distance between two points''' 111 | if longlat: 112 | try: 113 | az, _, dist = WGS84.inv(x0, y0, x1, y1) 114 | except NameError: 115 | raise NotImplementedError("Distance of long/lat coordinates requires pyproj.") 116 | 117 | else: 118 | az = azimuth(x0, y0, x1, y1, longlat=longlat) 119 | dist = distance(x0, y0, x1, y1, longlat=longlat) 120 | 121 | return az, dist 122 | 123 | 124 | def azimuth_distance_points(p0, p1, longlat=True): 125 | return azimuth_distance(p0[0], p0[1], p1[0], p1[1], longlat) 126 | 127 | 128 | def det(a, b): 129 | '''Determinant of a 2x2 matrix''' 130 | return a[0] * b[1] - a[1] * b[0] 131 | 132 | 133 | def onsegment(segment, point): 134 | '''Return True if point lies on segment''' 135 | xs, ys = tuple(zip(*segment)) 136 | pbr = point[0], point[1], point[0], point[1] 137 | 138 | if not intersectingbounds((min(xs), min(ys), max(xs), max(ys)), pbr): 139 | return False 140 | 141 | return (point[1] - ys[0]) * (xs[1] - xs[0]) == (ys[1] - ys[0]) * (point[0] - xs[0]) 142 | 143 | 144 | def intersectingbounds(mbr1, mbr2): 145 | '''Return True if two bounding boxes intersect, else False.''' 146 | # Check if intersection is impossible 147 | if mbr1[2] < mbr2[0] or mbr2[2] < mbr1[0] or mbr1[3] < mbr2[1] or mbr2[3] < mbr1[1]: 148 | return False 149 | else: 150 | return True 151 | 152 | 153 | def coincidentendpoint(linem, linen): 154 | '''Assuming segments linem and linem, return an overlapping point''' 155 | if onsegment(linem, linen[0]): 156 | return linen[0] 157 | elif onsegment(linem, linen[1]): 158 | return linen[1] 159 | elif onsegment(linen, linem[0]): 160 | return linem[0] 161 | elif onsegment(linen, linem[1]): 162 | return linem[1] 163 | return None 164 | 165 | 166 | def intersect(linem, linen, detm=None): 167 | ''' 168 | Check if two line segments intersect. Returns None or the intersection. 169 | If lines are coincident, returns one of the midpoints. 170 | 171 | Args: 172 | linem (Sequence): line segment 1, e.g. ((0, 0), (1, 1)) 173 | linen (Sequence): line segment 2, e.g. [[0, 1], [0, 1]] 174 | det (float): The determinant of linem. Precalculating might be useful if 175 | calculating intersection with the same segment many times 176 | ''' 177 | mx, my = tuple(zip(*linem)) 178 | nx, ny = tuple(zip(*linen)) 179 | 180 | mbrm = min(mx), min(my), max(mx), max(my) 181 | mbrn = min(nx), min(ny), max(nx), max(ny) 182 | 183 | if not intersectingbounds(mbrm, mbrn): 184 | return None 185 | 186 | xD = mx[0] - mx[1], nx[0] - nx[1] 187 | yD = my[0] - my[1], ny[0] - ny[1] 188 | 189 | div = det(xD, yD) 190 | 191 | if div == 0: 192 | try: 193 | # Check if lines are parallel 194 | if (xD[0] == xD[1] and yD[0] == yD[1]) or (yD[0] / xD[0] == yD[1] / float(xD[1])): 195 | return coincidentendpoint(linem, linen) 196 | 197 | except ZeroDivisionError: 198 | pass 199 | 200 | return None 201 | 202 | else: 203 | detm = detm or det(*linem) 204 | detn = det(*linen) 205 | x = det((detm, detn), xD) / float(div) 206 | y = det((detm, detn), yD) / float(div) 207 | 208 | if (intersectingbounds(mbrm, (x, y, x, y)) and 209 | intersectingbounds(mbrn, (x, y, x, y))): 210 | return x, y 211 | else: 212 | return None 213 | -------------------------------------------------------------------------------- /fionautil/layer.py: -------------------------------------------------------------------------------- 1 | # This file is part of fionautil. 2 | # http://github.com/fitnr/fionautil 3 | 4 | # Licensed under the GPLv3 license: 5 | # http://http://opensource.org/licenses/GPL-3.0 6 | # Copyright (c) 2015-6, Neil Freeman 7 | 8 | import itertools 9 | from functools import reduce 10 | import sys 11 | import fiona 12 | import fiona.transform 13 | 14 | try: 15 | from shapely.geometry import mapping, shape as shapelyshape 16 | except ImportError: 17 | pass 18 | from .geometry import disjointed 19 | from . import drivers 20 | 21 | 22 | def meta(filename): 23 | '''Return crs and schema for a layer''' 24 | with fiona.Env(): 25 | with fiona.open(filename, "r") as layer: 26 | return layer.meta 27 | 28 | 29 | def meta_complete(filename): 30 | '''Return crs and schema for a layer, as well as additional metadata.''' 31 | with fiona.Env(): 32 | with fiona.open(filename, "r") as layer: 33 | m = { 34 | 'bounds': layer.bounds, 35 | 'path': layer.path, 36 | 'name': layer.name, 37 | 'encoding': layer.encoding 38 | } 39 | m.update(layer.meta) 40 | return m 41 | 42 | 43 | def bounds(filename): 44 | '''Shortcut for returning bounds of a layer (minx, miny, maxx, maxy)''' 45 | with fiona.Env(): 46 | with fiona.open(filename, 'r') as layer: 47 | return layer.bounds 48 | 49 | 50 | def first(filename): 51 | '''Return the first feature of a layer''' 52 | with fiona.Env(): 53 | with fiona.open(filename, 'r') as layer: 54 | return next(iter(layer)) 55 | 56 | 57 | def fiter(filename): 58 | with fiona.Env(): 59 | with fiona.open(filename, "r") as layer: 60 | for feature in layer: 61 | yield feature 62 | 63 | 64 | def _fionaiter(iterfunc): 65 | def _iter(func, filename, *args, **kwargs): 66 | with fiona.Env(): 67 | with fiona.open(filename, "r") as layer: 68 | for feature in iterfunc(func, layer, *args, **kwargs): 69 | yield feature 70 | return _iter 71 | 72 | 73 | @_fionaiter 74 | def ffilter(func, layer, *args, **kwargs): 75 | return iter(f for f in layer if func(f, *args, **kwargs)) 76 | 77 | 78 | @_fionaiter 79 | def ffilterfalse(func, layer, *args, **kwargs): 80 | return iter(f for f in layer if not func(f, *args, **kwargs)) 81 | 82 | 83 | @_fionaiter 84 | def fmap(func, layer, *args, **kwargs): 85 | '''Yield features in a fiona layer, applying func to each''' 86 | return iter(func(f, *args, **kwargs) for f in layer) 87 | 88 | 89 | def freduce(func, filename, initializer=None): 90 | '''Reduce features of a layer to a single value''' 91 | with fiona.Env(): 92 | with fiona.open(filename, "r") as layer: 93 | return reduce(func, layer, initializer) 94 | 95 | 96 | def fchain(*filenames): 97 | '''Reduce features of a layer to a single value''' 98 | with fiona.Env(): 99 | for filename in itertools.chain(filenames): 100 | with fiona.open(filename, "r") as layer: 101 | for feature in layer: 102 | yield feature 103 | 104 | 105 | def fslice(filename, start, stop=None, step=None): 106 | if stop is None: 107 | stop = start 108 | start = None 109 | 110 | it = iter(range(start or 0, stop or sys.maxsize, step or 1)) 111 | try: 112 | nexti = next(it) 113 | except StopIteration: 114 | return 115 | with fiona.Env(): 116 | with fiona.open(filename, "r") as layer: 117 | for i, element in enumerate(layer): 118 | if i == nexti: 119 | yield element 120 | try: 121 | nexti = next(it) 122 | except StopIteration: 123 | return 124 | 125 | def fzip(*filenames): 126 | try: 127 | handles = [fiona.open(f) for f in filenames] 128 | for features in zip(*handles): 129 | yield features 130 | 131 | finally: 132 | for h in handles: 133 | h.close() 134 | 135 | 136 | def shapes(filename, crs=None): 137 | ''' 138 | Generator that yields a Shapely shape for every feature in a layer. 139 | ''' 140 | try: 141 | shapelyshape 142 | except NameError: 143 | raise NotImplementedError("length require shapely") 144 | with fiona.Env(): 145 | with fiona.open(filename, 'r') as layer: 146 | if crs is not None: 147 | def _geom(feature): 148 | return fiona.transform.transform_geom(layer.crs, crs, feature['geometry']) 149 | else: 150 | def _geom(feature): 151 | return feature['geometry'] 152 | 153 | for feature in layer: 154 | yield shapelyshape(_geom(feature)) 155 | 156 | 157 | def length(filename, crs=None): 158 | '''Get the length of a geodata file in its 159 | native projection or the given crs mapping''' 160 | geometries = shapes(filename, crs) 161 | return sum(x.length for x in geometries) 162 | 163 | 164 | def perimeter(filename, crs=None): 165 | '''Get perimeter of all features in a geodata file in its 166 | native projection or the given crs mapping''' 167 | geometries = shapes(filename, crs) 168 | return sum(x.boundary.length for x in geometries) 169 | 170 | 171 | def find(filename, key, value): 172 | '''Special case of ffilter: return the first feature where key==value''' 173 | def test(f): 174 | return f['properties'][key] == value 175 | return next(ffilter(test, filename)) 176 | 177 | 178 | def dissolve(sourcefile, sinkfile, key, unsplit=None): 179 | try: 180 | shape 181 | except NameError: 182 | raise NotImplementedError("dissolve require shapely") 183 | 184 | with fiona.Env(): 185 | with fiona.open(sourcefile) as source: 186 | schema = source.schema 187 | schema['properties'] = {key: source.schema['properties'][key]} 188 | 189 | with fiona.open(sinkfile, 'w', crs=source.crs, schema=schema, driver=source.driver) as sink: 190 | 191 | gotkeys = dict() 192 | 193 | for _, feat in source.items(): 194 | fkey = feat['properties'][key] 195 | fshape = shapelyshape(feat['geometry']) 196 | 197 | if fkey in gotkeys: 198 | gotkeys[fkey][0] = gotkeys[fkey][0].union(fshape) 199 | else: 200 | gotkeys[fkey] = [fshape] 201 | 202 | for shapelist in gotkeys.values(): 203 | if unsplit: 204 | for s in disjointed(shapelist): 205 | sink.write(s) 206 | 207 | else: 208 | sink.write(shapelist[0]) 209 | 210 | 211 | def create(output, geometries, properties=None, crs=None, driver=None): 212 | ''' 213 | Create a layer from a set of shapely geometries or geometry 214 | dicts. Use list of properties (dict) if provided, otherwise an index of list as an ID.''' 215 | try: 216 | schema = {'geometry': geometries[0].type} 217 | except AttributeError: 218 | schema = {'geometry': geometries[0]['type']} 219 | 220 | driver = driver or drivers.from_path(output) 221 | 222 | FIELD_MAP = {v: k for k, v in fiona.FIELD_TYPES_MAP.items()} 223 | 224 | if properties: 225 | schema['properties'] = {k: FIELD_MAP[type(v)] for k, v in properties[0].items()} 226 | else: 227 | schema['properties'] = {'id': 'int'} 228 | properties = [{'id': x} for x in range(len(geometries))] 229 | 230 | with fiona.Env(): 231 | with fiona.open(output, 'w', driver=driver, crs=crs, schema=schema) as f: 232 | for geom, props in zip(geometries, properties): 233 | try: 234 | feature = {'properties': props, 'geometry': mapping(geom)} 235 | except AttributeError: 236 | feature = {'properties': props, 'geometry': geom} 237 | 238 | f.write(feature) 239 | -------------------------------------------------------------------------------- /tests/test_geometry.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import fionautil.geometry 3 | 4 | 5 | class GeometryTestCase(unittest.TestCase): 6 | 7 | def setUp(self): 8 | self.polygon = {'type': 'Polygon', 'coordinates': [[(-122.002301, 37.529835999999996), (-122.002236, 37.529908), (-122.002167, 37.52999), (-122.00213600000001, 37.530032999999996), (-122.002088, 37.530104), (-122.001919, 37.530451), (-122.00178, 37.530736999999995), (-122.001739, 37.530865), (-122.001711, 37.530983), (-122.001694, 37.531085), (-122.001653, 37.531376), (-122.001648, 37.531425), (-122.001639, 37.53153), (-122.00163599999999, 37.531572), (-122.001634, 37.531622), (-122.001632, 37.531647), (-122.001628, 37.531724), (-122.001627, 37.531749999999995), (-122.001616, 37.531938), (-122.001611, 37.532042), (-122.001595, 37.532178), (-122.001571, 37.532319), (-122.00153, 37.532484), (-121.992616, 37.524805), (-121.992876, 37.524935), (-121.993034, 37.525014), (-121.993212, 37.5251), (-121.993746, 37.52536), (-121.99392399999999, 37.525447), (-121.99403, 37.525498999999996), (-121.994349, 37.525656), (-121.994456, 37.525709), (-121.994869, 37.525912999999996), (-121.995685, 37.526316), (-121.995711, 37.526329), (-121.99611, 37.526525), (-121.99652499999999, 37.526728999999996), (-121.996576, 37.526754), (-121.996729, 37.526829), (-121.996781, 37.526855), (-121.996846, 37.526886999999995), (-121.997044, 37.526984), (-121.99710999999999, 37.527017), (-121.997145, 37.527034), (-121.997251, 37.527086), (-121.997287, 37.527104), (-121.99733, 37.527125), (-121.99745899999999, 37.527187999999995), (-121.997503, 37.52721), (-121.99810000000001, 37.527508), (-121.998488, 37.527695), (-121.998568, 37.527733999999995), (-121.998795, 37.527848999999996), (-122.001394, 37.529249), (-122.001506, 37.529309999999995), (-122.002196, 37.529679), (-122.00236, 37.529773999999996), (-122.002301, 37.529835999999996)]]} 9 | 10 | self.linestring = {'type': 'LineString', 'coordinates': [(1698558.6560016416, 785359.5722958631), (1698570.6255255828, 785357.1199829046), (1698592.7188758815, 785353.618717982), (1698615.888545638, 785342.774320389), (1698632.1053597904, 785336.6905814429), (1698651.8725316962, 785330.0421180109), (1698659.045153662, 785327.3537529241), (1698688.1001785155, 785316.4641486479), (1698696.6319864516, 785313.4574198754), (1698717.936275069, 785303.1641204398), (1698733.601512783, 785295.2805483269), (1698757.090320928, 785281.0787615285), (1698773.2214488257, 785270.9157159382), (1698802.13278612, 785251.4878775944), (1698839.0495838472, 785227.758052662), (1701103.0715925542, 785570.0575142633), (1701117.0415831083, 785569.3192712615), (1701143.1992729052, 785566.1717272209), (1701168.650131336, 785560.3315072984), (1701209.706061085, 785548.4685232819), (1701250.512021864, 785532.6336759274), (1701281.5801281473, 785515.853762881), (1701310.5929042343, 785499.6991391898), (1701378.6116007813, 785459.892916804), (1701445.1270219584, 785422.3276661132), (1701482.1400952877, 785402.5158508102), (1701506.6506188242, 785391.8440056049), (1701523.390860305, 785385.4644141722), (1701559.0541053142, 785373.9359225394), (1701572.477673862, 785369.6783910012)]} 11 | 12 | self.epsgpoly = [(1867225.3631415186, 615357.5771661324), (1867231.2356635556, 615365.4745198288), (1867237.479480026, 615374.4758658895), (1867240.2956402097, 615379.2034383449), (1867244.6640119287, 615387.0139517855), (1867260.2170524723, 615425.2795272444), (1867273.010158651, 615456.8187370438), (1867276.8613616587, 615470.9642739793), (1867279.5459099195, 615484.0185862744), (1867281.2298689168, 615495.3130386977), (1867285.371296512, 615527.5460854613), (1867285.9004078459, 615532.9763432294), (1867286.8827223033, 615544.6150411123), (1867287.2226247364, 615549.2713711904), (1867287.4884011522, 615554.816849761), (1867287.7096603888, 615557.5881709672), (1867288.2002566787, 615566.1269034279), (1867288.3349251715, 615569.010609063), (1867289.6417699724, 615589.8566742853), (1867290.268811733, 615601.3900791614), (1867291.9249083567, 615616.458807302), (1867294.2968654872, 615632.0710232898), (1867298.2138406327, 615650.3223257487), (1868072.3540395212, 614785.6087283635), (1868049.6055425154, 614800.4009144845), (1868035.7814955255, 614809.390042857), (1868020.2022930041, 614819.1841641953), (1867973.4684487754, 614848.7886375139), (1867957.8911653375, 614858.6938429191), (1867948.6151616173, 614864.6136581865), (1867920.7006236156, 614882.4855442767), (1867911.3380669476, 614888.517779165), (1867875.1994654038, 614911.7379168055), (1867803.7977765708, 614957.6097095839), (1867801.523018313, 614959.0889994473), (1867766.6081204978, 614981.402105804), (1867730.2935749195, 615004.625704356), (1867725.8307135513, 615007.4719455142), (1867712.4421478317, 615016.0106834827), (1867707.8926967685, 615018.9693142391), (1867702.2049999998, 615022.6121230425), (1867684.8785847616, 615033.655780871), (1867679.1043068136, 615037.4109857872), (1867676.0413041625, 615039.3469008117), (1867666.7657036753, 615045.267033074), (1867663.6161057586, 615047.3153330811), (1867659.8532002757, 615049.7064320575), (1867648.5644968417, 615056.879739293), (1867644.7149993733, 615059.3832257404), (1867592.4836593368, 615093.2956794645), (1867558.5260306897, 615114.5954067486), (1867551.525267213, 615119.0363117309), (1867531.6683507564, 615132.1187283283), (1867304.4725995616, 615291.1535361662), (1867294.683327623, 615298.0812909331), (1867234.3626622162, 615340.006498295), (1867220.038663901, 615350.7809663619), (1867225.3631415186, 615357.5771661324)] 13 | 14 | self.multipolygon = {'type': 'MultiPolygon', 'coordinates': [[[(-156.71787267699898, 21.137419760764438), (-156.76604819764898, 21.06517681773707), (-156.8864840998058, 21.04913411712499), (-157.07113192831787, 21.105330956858555), (-157.28789752278652, 21.081250571840183), (-157.30394879695828, 21.137448432663767), (-157.2477475061229, 21.161530451508398), (-157.23169415834994, 21.233776323139818), (-157.16747148788883, 21.19364045634365), (-157.00690374246722, 21.18561063798249), (-156.95873348755612, 21.209693513430512), (-156.94268251253186, 21.161526867134565), (-156.71787267699898, 21.137419760764438)]], [[(-156.1960454124824, 20.631649407365213), (-156.27631740739284, 20.583483860915248), (-156.3967349429455, 20.567426981472988), (-156.43687923087785, 20.623621217336662), (-156.46097189319752, 20.727981087256364), (-156.49307474018514, 20.792204281510333), (-156.5251935717042, 20.776149657497466), (-156.63758660416465, 20.80826091204387), (-156.69378263402297, 20.912624010061208), (-156.65363600731771, 21.016985049562912), (-156.5974396197875, 21.041064705415824), (-156.5251916458635, 20.98487016377238), (-156.47702205254387, 20.89656513911172), (-156.3566035032919, 20.9447263610079), (-156.26026336307226, 20.928671268747912), (-156.01139245254785, 20.800225321607478), (-155.98731705257018, 20.752061631687628), (-156.0435115425545, 20.65573259996677), (-156.1318089039728, 20.62362291867985), (-156.1960454124824, 20.631649407365213)]], [[(-157.03905067093797, 20.928706972385005), (-156.91058584516276, 20.928718617694337), (-156.80620534154883, 20.84041861217258), (-156.81422569781648, 20.7922527172797), (-156.88648717839553, 20.73604940916775), (-156.96676559866026, 20.728020731638775), (-156.9908634657756, 20.792237151462064), (-156.9828263229743, 20.832377626807837), (-157.05509821197901, 20.880538425907034), (-157.03905067093797, 20.928706972385005)]]]} 15 | 16 | def testRoundGeometry(self): 17 | z = fionautil.geometry.roundgeometry(self.linestring, 0) 18 | fixture = tuple(round(i, 0) for i in self.linestring['coordinates'][0]) 19 | try: 20 | assert z['coordinates'][0] == fixture 21 | except ValueError: 22 | self.assertSequenceEqual(z['coordinates'][0].tolist(), fixture) 23 | 24 | y = fionautil.geometry.roundgeometry(self.polygon, 0) 25 | fixture = tuple(round(i, 0) for i in self.polygon['coordinates'][0][0]) 26 | try: 27 | assert y['coordinates'][0][0] == fixture 28 | except ValueError: 29 | self.assertSequenceEqual(y['coordinates'][0][0].tolist(), fixture) 30 | 31 | x = fionautil.geometry.roundgeometry(self.multipolygon, 0) 32 | fixture = tuple(round(i, 0) for i in self.multipolygon['coordinates'][0][0][0]) 33 | try: 34 | self.assertSequenceEqual(x['coordinates'][0][0][0], fixture) 35 | except ValueError: 36 | self.assertSequenceEqual(x['coordinates'][0][0][0].tolist(), fixture) 37 | 38 | w = fionautil.geometry.roundgeometry({'type': 'Point', 'coordinates': [12.345, 67.890]}, 0) 39 | try: 40 | self.assertSequenceEqual(w['coordinates'], (12., 68.0)) 41 | except ValueError: 42 | self.assertSequenceEqual(w['coordinates'].tolist(), (12., 68.0)) 43 | 44 | def test_explodepoints_polygon(self): 45 | explode = fionautil.geometry.explodepoints(self.polygon) 46 | self.assertEqual(next(explode), self.polygon['coordinates'][0][0]) 47 | self.assertEqual(next(explode), self.polygon['coordinates'][0][1]) 48 | 49 | def test_explodepoints_linestring(self): 50 | explode = fionautil.geometry.explodepoints(self.linestring) 51 | self.assertEqual(next(explode), self.linestring['coordinates'][0]) 52 | self.assertEqual(next(explode), self.linestring['coordinates'][1]) 53 | 54 | def test_exploderings_polygon(self): 55 | explode = fionautil.geometry.exploderings(self.polygon) 56 | self.assertEqual(next(explode), self.polygon['coordinates'][0]) 57 | 58 | def test_exploderings_linestring(self): 59 | explode = fionautil.geometry.exploderings(self.linestring) 60 | self.assertEqual(next(explode), self.linestring['coordinates']) 61 | self.assertRaises(StopIteration, next, explode) 62 | 63 | def test_endpoints(self): 64 | self.assertRaises(ValueError, fionautil.geometry.endpoints, self.polygon) 65 | self.assertEqual(fionautil.geometry.endpoints(self.linestring), (self.linestring['coordinates'][0], self.linestring['coordinates'][-1])) 66 | 67 | def testExplodeValueError(self): 68 | objfoo = {'type': 'Foo', 'coordinates': [(1, 0), (2, 2), (3, 4)]} 69 | self.assertRaises(ValueError, next, fionautil.geometry.explodepoints(objfoo)) 70 | self.assertRaises(ValueError, next, fionautil.geometry.explodesegments(objfoo)) 71 | self.assertRaises(ValueError, next, fionautil.geometry.exploderings(objfoo)) 72 | 73 | def testExplodesegmentsPoly(self): 74 | exploded = fionautil.geometry.explodesegments(self.polygon) 75 | 76 | self.assertEqual(next(exploded), tuple(self.polygon['coordinates'][0][0:2])) 77 | self.assertEqual(next(exploded), tuple(self.polygon['coordinates'][0][1:3])) 78 | 79 | def testExplodesegmentsLineString(self): 80 | exploded = fionautil.geometry.explodesegments(self.linestring) 81 | 82 | self.assertEqual(next(exploded), tuple(self.linestring['coordinates'][0:2])) 83 | self.assertEqual(next(exploded), tuple(self.linestring['coordinates'][1:3])) 84 | 85 | def testExplodesegmentsMultipoly(self): 86 | exploded = fionautil.geometry.explodesegments(self.multipolygon) 87 | 88 | self.assertEqual(next(exploded), tuple(self.multipolygon['coordinates'][0][0][0:2])) 89 | self.assertEqual(next(exploded), tuple(self.multipolygon['coordinates'][0][0][1:3])) 90 | 91 | 92 | if __name__ == '__main__': 93 | unittest.main() 94 | --------------------------------------------------------------------------------