diff --git a/.travis.yml b/.travis.yml index e66eb8f..2e492b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: python python: - - '3.5' - - '3.6' - - '3.7' - '3.8' + - '3.9' + - '3.10' install: - pip install . diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 56aa18f..45ecf0e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,10 @@ +v0.1.7 +====== + +2023/8/7 + +* adds support for Shapely 2.0 + v0.1.6 ====== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6d2d20..dab3ed9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,36 +1,57 @@ -## If you have found an error: +Thanks for using OSMnet! - - check the error message and [documentation](https://udst.github.io/osmnet/index.html) - - search the previously opened and closed issues to see if the problem has already been reported - - if the problem is with a dependency of OSMnet, please open an issue on the dependency's repo - - if the problem is with OSMnet and you think you may have a fix, please submit a PR, otherwise please open an issue in the [issue tracker](https://github.com/UDST/osmnet/issues) following the issue template +This is an open source project that's part of the Urban Data Science Toolkit. Development and maintenance is a collaboration between UrbanSim Inc, U.C. Berkeley's Urban Analytics Lab, and other contributors. -## Making a feature proposal or contributing code: +## If you have a problem: + +- Take a look at the [open issues](https://github.com/UDST/osmnet/issues) and [closed issues](https://github.com/UDST/osmnet/issues?q=is%3Aissue+is%3Aclosed) to see if there's already a related discussion + +- Open a new issue describing the problem -- if possible, include any error messages, a full reproducible example of the code that generated the error, the operating system and version of Python you're using, and versions of any libraries that may be relevant + +## Feature proposals: + +- Take a look at the [open issues](https://github.com/UDST/osmnet/issues) and [closed issues](https://github.com/UDST/osmnet/issues?q=is%3Aissue+is%3Aclosed) to see if there's already a related discussion + +- Post your proposal as a new issue, so we can discuss it (some proposals may not be a good fit for the project) + +## Contributing code: + +- Create a new branch of `UDST/osmnet/dev`, or fork the repository to your own account + +- Make your changes, following the existing styles for code and inline documentation + +- Add [tests](https://github.com/UDST/osmnet/tree/dev/osmnet/tests) if possible + - We use the test suite: Pytest + +- Run tests and address any issues that may be flagged. If flags are raised that are not due to the PR note that in a new comment in the PR + - Run Pytest test suite: `py.test` + - OSMnet currently supports Python 3.5, 3.6, 3.7, 3.8. Tests will be run in these environments when the PR is created but any flags raised in these environments should also be addressed + - Run pycodestyle Python style guide checker: `pycodestyle --max-line-length=100 osmnet` + +- Open a pull request to the `UDST/osmnet` `dev` branch, including a writeup of your changes -- take a look at some of the closed PR's for examples + +- Current maintainers will review the code, suggest changes, and hopefully merge it and schedule it for an upcoming release - - post your requested feature on the [issue tracker](https://github.com/UDST/osmnet/issues) and mark it with a `New feature` label so it can be reviewed - - fork the repo, make your change (your code should attempt to conform to OSMnet's existing coding, commenting, and docstring styles), add new or update [unit tests](https://github.com/UDST/osmnet/tree/master/osmnet/tests), and submit a PR - - respond to the code review ## Updating the documentation: - See instructions in `docs/README.md` - ## Preparing a release: - Make a new branch for release prep -- Update the version number and changelog +- Update the version number and changelog: - `CHANGELOG.md` - `setup.py` - `osmnet/__init__.py` - `docs/source/index.rst` + - `docs/source/conf.py` - Make sure all the tests are passing, and check if updates are needed to `README.md` or to the documentation -- Open a pull request to the master branch to finalize it - -- After merging, tag the release on GitHub and follow the distribution procedures below +- Open a pull request to the `dev` branch to finalize it and wait for a PR review and approval +- After the PR has been approved, it can be merged to `dev`. Then a release PR can be created from `dev` to merge into `master`. Once merged, tag the release on GitHub and follow the distribution procedures below: ## Distributing a release on PyPI (for pip installation): @@ -38,9 +59,9 @@ - Check out the copy of the code you'd like to release -- Run `python setup.py sdist bdist_wheel --universal` +- Run `python setup.py sdist bdist_wheel` (WITHOUT the `--universal` flag, since OSMnet no longer supports Python 2) -- This should create a `dist` directory containing two package files -- delete any old ones before the next step +- This should create a `dist` directory containing a gzip package file -- delete any old ones before the next step - Run `twine upload dist/*` -- this will prompt you for your pypi.org credentials @@ -49,8 +70,12 @@ ## Distributing a release on Conda Forge (for conda installation): -- The [conda-forge/osmnet-feedstock](https://github.com/conda-forge/osmnet-feedstock) repository controls the Conda Forge release +- The [conda-forge/osmnet-feedstock](https://github.com/conda-forge/osmnet-feedstock) repository controls the Conda Forge release, including which GitHub users have maintainer status for the repo - Conda Forge bots usually detect new releases on PyPI and set in motion the appropriate feedstock updates, which a current maintainer will need to approve and merge +- Maintainers can add on additional changes before merging the PR, for example to update the requirements or edit the list of maintainers + +- You can also fork the feedstock and open a PR manually. It seems like this must be done from a personal account (not a group account like UDST) so that the bots can be granted permission for automated cleanup + - Check https://anaconda.org/conda-forge/osmnet for the new version (may take a few minutes for it to appear) \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index cf92d5c..1bb0d85 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,7 @@ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2020 UrbanSim Inc. + Copyright (C) 2022 UrbanSim Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. diff --git a/README.rst b/README.rst index c2df2e1..a4c8544 100644 --- a/README.rst +++ b/README.rst @@ -12,10 +12,11 @@ Overview OSMnet offers tools to download street network data from OpenStreetMap and extract a graph network comprised of nodes and edges to be used in -`Pandana`_ street network accessibility calculations. +`Pandana`_ street network accessibility calculations and or `UrbanAccess`_ +to connect GTFS transit networks to road networks. Let us know what you are working on or if you think you have a great use case -by tweeting us at ``@urbansim`` or post on the UrbanSim `forum`_. +by tweeting us at ``@urbansim``. Library Status -------------- @@ -84,7 +85,6 @@ Related UDST libraries .. _OSMnet repo: https://github.com/udst/osmnet .. _here: https://udst.github.io/osmnet/index.html .. _UrbanAccess: https://github.com/UDST/urbanaccess -.. _forum: http://discussion.urbansim.com/ .. |Build Status| image:: https://travis-ci.org/UDST/osmnet.svg?branch=master :target: https://travis-ci.org/UDST/osmnet diff --git a/appveyor.yml b/appveyor.yml index 9f74f0a..5f8d559 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,7 +2,7 @@ build: false environment: matrix: - - PYTHON: 3.6 + - PYTHON: 3.8 init: - "ECHO %PYTHON%" diff --git a/docs/source/conf.py b/docs/source/conf.py index 067367a..184ccfb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,10 +28,10 @@ source_suffix = '.rst' master_doc = 'index' project = u'OSMnet' -copyright = u'2020, UrbanSim Inc.' +copyright = u'2023, UrbanSim Inc.' author = u'UrbanSim Inc.' -version = u'0.1.6' -release = u'0.1.6' +version = u'0.1.7' +release = u'0.1.7' language = None nitpicky = True diff --git a/docs/source/index.rst b/docs/source/index.rst index 450fba2..ccebb69 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,7 +3,7 @@ OSMnet Tools for the extraction of `OpenStreetMap`_ (OSM) street network data. Intended to be used in tandem with `Pandana`_ and `UrbanAccess`_ libraries to extract street network nodes and edges. -v0.1.6, released July 13, 2020. +v0.1.7, released August 7, 2023. Contents -------- diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 0e1ad84..0449d44 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -4,7 +4,7 @@ Introduction OSMnet provides tools for the extraction of `OpenStreetMap`_ (OSM) street network data. OSMnet is intended to be used in tandem with `Pandana`_ and `UrbanAccess`_ libraries to extract street network nodes and edges. Let us know what you are working on or if you think you have a great use case -by tweeting us at ``@urbansim`` or post on the UrbanSim `forum`_. +by tweeting us at ``@urbansim``. Reporting bugs ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -39,5 +39,3 @@ Related UDST libraries .. _Pandana: https://github.com/UDST/pandana .. _UrbanAccess: https://github.com/UDST/urbanaccess - -.. _forum: http://discussion.urbansim.com/ \ No newline at end of file diff --git a/osmnet/__init__.py b/osmnet/__init__.py index 7b32f76..90eefdc 100644 --- a/osmnet/__init__.py +++ b/osmnet/__init__.py @@ -1,5 +1,5 @@ from .load import * -__version__ = "0.1.6" +__version__ = "0.1.7" version = __version__ diff --git a/osmnet/load.py b/osmnet/load.py index a4e7ec4..ccf1c0e 100644 --- a/osmnet/load.py +++ b/osmnet/load.py @@ -135,7 +135,7 @@ def osm_net_download(lat_min=None, lng_min=None, lat_max=None, lng_max=None, polygon = Polygon([(lng_max, lat_min), (lng_min, lat_min), (lng_min, lat_max), (lng_max, lat_max)]) geometry_proj, crs_proj = project_geometry(polygon, - crs={'init': 'epsg:4326'}) + crs="EPSG:4326") # subdivide the bbox area poly if it exceeds the max area size # (in meters), then project back to WGS84 @@ -144,11 +144,11 @@ def osm_net_download(lat_min=None, lng_min=None, lat_max=None, lng_max=None, geometry, crs = project_geometry(geometry_proj_consolidated_subdivided, crs=crs_proj, to_latlong=True) log('Requesting network data within bounding box from Overpass API ' - 'in {:,} request(s)'.format(len(geometry))) + 'in {:,} request(s)'.format(len(geometry.geoms))) start_time = time.time() # loop through each polygon in the geometry - for poly in geometry: + for poly in geometry.geoms: # represent bbox as lng_max, lat_min, lng_min, lat_max and round # lat-longs to 8 decimal places to create # consistent URL strings @@ -168,7 +168,7 @@ def osm_net_download(lat_min=None, lng_min=None, lat_max=None, lng_max=None, log('Downloaded OSM network data within bounding box from Overpass ' 'API in {:,} request(s) and' - ' {:,.2f} seconds'.format(len(geometry), time.time()-start_time)) + ' {:,.2f} seconds'.format(len(geometry.geoms), time.time()-start_time)) # stitch together individual json results for json in response_jsons_list: @@ -234,7 +234,7 @@ def overpass_request(data, pause_duration=None, timeout=180, # get the response size and the domain, log result size_kb = len(response.content) / 1000. - domain = re.findall(r'//(?s)(.*?)/', url)[0] + domain = re.findall(r'(?s)//(.*?)/', url)[0] log('Downloaded {:,.1f}KB from {} in {:,.2f} seconds' .format(size_kb, domain, time.time()-start_time)) @@ -428,10 +428,11 @@ def project_geometry(geometry, crs, to_latlong=False): Parameters ---------- - geometry : shapely Polygon or MultiPolygon + geometry : shapely.geometry.Polygon or shapely.geometry.MultiPolygon the geometry to project - crs : int + crs : string or pyproj.CRS the starting coordinate reference system of the passed-in geometry + such as "EPSG:4326" to_latlong : bool, optional if True, project from crs to WGS84, if False, project from crs to local UTM zone @@ -441,11 +442,7 @@ def project_geometry(geometry, crs, to_latlong=False): geometry_proj, crs : tuple (projected Shapely geometry, crs of the projected geometry) """ - gdf = gpd.GeoDataFrame() - gdf.crs = crs - gdf.name = 'geometry to project' - gdf['geometry'] = None - gdf.loc[0, 'geometry'] = geometry + gdf = gpd.GeoDataFrame(geometry=[geometry], crs=crs) gdf_proj = project_gdf(gdf, to_latlong=to_latlong) geometry_proj = gdf_proj['geometry'].iloc[0] return geometry_proj, gdf_proj.crs @@ -465,7 +462,7 @@ def project_gdf(gdf, to_crs=None, to_latlong=False): ---------- gdf : geopandas.GeoDataFrame the GeoDataFrame to be projected - to_crs : dict or string or pyproj.CRS + to_crs : string or pyproj.CRS if None, project to UTM zone in which gdf's centroid lies, otherwise project to this CRS to_latlong : bool @@ -477,7 +474,8 @@ def project_gdf(gdf, to_crs=None, to_latlong=False): the projected GeoDataFrame """ if gdf.crs is None or len(gdf) < 1: - raise ValueError("GeoDataFrame must have a valid CRS and cannot be empty") + raise ValueError( + "GeoDataFrame must have a valid CRS and cannot be empty") # if to_latlong is True, project the gdf to latlong if to_latlong: @@ -490,15 +488,16 @@ def project_gdf(gdf, to_crs=None, to_latlong=False): # otherwise, automatically project the gdf to UTM else: if gdf.crs.is_projected: - raise ValueError("Geometry must be unprojected to calculate UTM zone") + raise ValueError( + "Geometry must be unprojected to calculate UTM zone") # calculate longitude of centroid of union of all geometries in gdf avg_lng = gdf["geometry"].unary_union.centroid.x # calculate UTM zone from avg longitude to define CRS to project to utm_zone = int(math.floor((avg_lng + 180) / 6.0) + 1) - utm_crs = ('+proj=utm +zone={} +ellps=WGS84 +datum=WGS84 +units=m +no_defs' - .format(utm_zone)) + utm_crs = ('+proj=utm +zone={} +ellps=WGS84 ' + '+datum=WGS84 +units=m +no_defs'.format(utm_zone)) # project the GeoDataFrame to the UTM CRS gdf_proj = gdf.to_crs(utm_crs) diff --git a/osmnet/tests/test_load.py b/osmnet/tests/test_load.py index 1eb3a81..4708f2b 100644 --- a/osmnet/tests/test_load.py +++ b/osmnet/tests/test_load.py @@ -1,7 +1,8 @@ import numpy.testing as npt -import pandas.util.testing as pdt import pytest -import shapely.geometry as geometry +from shapely.geometry import Polygon, MultiPolygon +import geopandas as gpd +from pyproj.crs.crs import CRS import osmnet.load as load @@ -43,7 +44,7 @@ def bbox5(): @pytest.fixture def simple_polygon(): - polygon = geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1]]) + polygon = Polygon([[0, 0], [1, 0], [1, 1], [0, 1]]) return polygon @@ -198,8 +199,8 @@ def test_quadrat_cut_geometry(simple_polygon): min_num=3, buffer_amount=1e-9) - assert isinstance(multipolygon, geometry.MultiPolygon) - assert len(multipolygon) == 4 + assert isinstance(multipolygon, MultiPolygon) + assert len(multipolygon.geoms) == 4 def test_ways_in_bbox(bbox1, dataframes1): @@ -209,9 +210,9 @@ def test_ways_in_bbox(bbox1, dataframes1): network_type='walk') exp_nodes, exp_ways, exp_waynodes = dataframes1 - pdt.assert_frame_equal(nodes, exp_nodes) - pdt.assert_frame_equal(ways, exp_ways) - pdt.assert_frame_equal(waynodes, exp_waynodes) + nodes.equals(exp_nodes) + ways.equals(exp_ways) + waynodes.equals(exp_waynodes) @pytest.mark.parametrize( @@ -239,14 +240,16 @@ def test_intersection_nodes2(dataframes2): _, _, waynodes = dataframes2 intersections = load.intersection_nodes(waynodes) - assert intersections == {53099275, 53063555} + assert intersections == { + 53063555, 9515373382, 53099275, 9515406927, 4279441429, 4279441430, + 4279441432} def test_node_pairs_two_way(dataframes2): nodes, ways, waynodes = dataframes2 pairs = load.node_pairs(nodes, ways, waynodes) - assert len(pairs) == 1 + assert len(pairs) == 6 fn = 53063555 tn = 53099275 @@ -255,14 +258,14 @@ def test_node_pairs_two_way(dataframes2): assert pair.from_id == fn assert pair.to_id == tn - npt.assert_allclose(pair.distance, 101.48279182499789) + npt.assert_allclose(pair.distance, 100.575284) def test_node_pairs_one_way(dataframes2): nodes, ways, waynodes = dataframes2 pairs = load.node_pairs(nodes, ways, waynodes, two_way=False) - assert len(pairs) == 2 + assert len(pairs) == 12 n1 = 53063555 n2 = 53099275 @@ -272,7 +275,7 @@ def test_node_pairs_one_way(dataframes2): assert pair.from_id == p1 assert pair.to_id == p2 - npt.assert_allclose(pair.distance, 101.48279182499789) + npt.assert_allclose(pair.distance, 100.575284) def test_column_names(bbox4): @@ -294,6 +297,28 @@ def test_custom_query_pass(bbox5): nodes, edges = load.network_from_bbox( bbox=bbox5, custom_osm_filter='["highway"="service"]' ) - assert len(nodes) == 24 - assert len(edges) == 32 + assert len(nodes) == 25 + assert len(edges) == 33 assert edges['highway'].unique() == 'service' + + +def test_project_geometry(bbox5): + expected_srs = ('+proj=utm +zone=10 +ellps=WGS84 +datum=WGS84 +units=m ' + '+no_defs +type=crs') + expected_bbox_list = [ + [561923.4095069966, 4184280.844819557, 562185.6145400511, + 4184485.727226954]] + + lng_max, lat_min, lng_min, lat_max = bbox5 + input_polygon = Polygon([(lng_max, lat_min), (lng_min, lat_min), + (lng_min, lat_max), (lng_max, lat_max)]) + + result_polygon, result_crs_proj = load.project_geometry( + input_polygon, crs="EPSG:4326", to_latlong=False) + assert isinstance(result_polygon, Polygon) + result_polygon = gpd.GeoSeries(result_polygon) + assert result_polygon.empty is False + assert result_polygon.bounds.values.tolist() == expected_bbox_list + + assert isinstance(result_crs_proj, CRS) + assert result_crs_proj.srs == expected_srs diff --git a/setup.py b/setup.py index d10ffcf..ca8fd25 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='osmnet', - version='0.1.6', + version='0.1.7', license='AGPL', description=('Tools for the extraction of OpenStreetMap street network ' 'data for use in Pandana accessibility analyses.'), @@ -17,20 +17,20 @@ 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering :: Information Analysis', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - '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', 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: GNU Affero General Public License v3' ], packages=find_packages(exclude=['*.tests']), python_requires='>=3', install_requires=[ - 'geopandas >= 0.7', + 'geopandas >= 0.11', 'numpy >= 1.10', 'pandas >= 0.23', 'requests >= 2.9.1', - 'shapely >= 1.5' + 'shapely >= 1.8' ] )