osmexp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
osmexp/__init__.py ADDED
File without changes
osmexp/__main__.py ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python
2
+
3
+ import sys
4
+
5
+ from .geojson import export_node, export_relation, export_way
6
+
7
+ HELP = """
8
+ Usage: osmexp [element] [id]
9
+
10
+ Arguments:
11
+ element: The OSM element type, options: "rel", "way" or "node".
12
+ id: The OSM element ID: an integer number.
13
+
14
+ Output: A GeoJSON representation of the element.
15
+ """
16
+
17
+ def main():
18
+
19
+ if len(sys.argv) != 3 or (len(sys.argv) == 2 and sys.argv[1] in ['-h', '--help']):
20
+ print(HELP)
21
+ sys.exit()
22
+
23
+ if sys.argv[1].lower() not in ['rel', 'way', 'node']:
24
+ print('Unknown element type, please use "rel", "way" or "node".')
25
+ sys.exit()
26
+
27
+ if not sys.argv[2].isdigit():
28
+ print('Element ID should be an integer.')
29
+ sys.exit()
30
+
31
+ element_type = sys.argv[1].lower()
32
+ element_id = sys.argv[2]
33
+
34
+ match element_type:
35
+ case 'node':
36
+ print(export_node(element_id))
37
+ case 'way':
38
+ print(export_way(element_id))
39
+ case 'rel':
40
+ print(export_relation(element_id))
41
+
42
+
43
+ if __name__ == '__main__':
44
+ main()
osmexp/fetcher.py ADDED
@@ -0,0 +1,40 @@
1
+ import itertools
2
+ from http import HTTPStatus
3
+
4
+ import requests
5
+ from shapely import get_parts, polygonize_full
6
+ from shapely.geometry import GeometryCollection, LineString, Point, Polygon
7
+
8
+ API_URL = 'https://www.openstreetmap.org/api/0.6'
9
+
10
+
11
+ def parse_nodes_ways(elements: list[dict]) -> tuple[list[dict], list[dict]]:
12
+ nodes = {e['id']: Point(e['lon'], e['lat']) for e in elements if e['type'] == 'node'}
13
+ ways = [LineString([nodes[n] for n in e['nodes']]) for e in elements if e['type'] == 'way']
14
+ return list(nodes.values()), ways
15
+
16
+
17
+ def fetch_node(node_id: int) -> Point | None:
18
+ response = requests.get(f'{API_URL}/node/{node_id}.json')
19
+ if response.status_code != HTTPStatus.OK:
20
+ return None
21
+ return parse_nodes_ways(response.json()['elements'])[0][0]
22
+
23
+
24
+ def fetch_way(way_id: int) -> LineString | Polygon | None:
25
+ response = requests.get(f'{API_URL}/way/{way_id}/full.json')
26
+ if response.status_code != HTTPStatus.OK:
27
+ return None
28
+ _, ways = parse_nodes_ways(response.json()['elements'])
29
+ way = ways[0]
30
+ return Polygon(way) if way.is_closed and way.is_ring else way
31
+
32
+
33
+ def fetch_relation(relation_id: int) -> GeometryCollection | None:
34
+ response = requests.get(f'{API_URL}/relation/{relation_id}/full.json')
35
+ if response.status_code != HTTPStatus.OK:
36
+ return None
37
+ _, ways = parse_nodes_ways(response.json()['elements'])
38
+ cases = [get_parts(p) for p in polygonize_full(ways) if p]
39
+ parts = list(itertools.chain(*cases))
40
+ return GeometryCollection(parts)
osmexp/geojson.py ADDED
@@ -0,0 +1,45 @@
1
+ import json
2
+
3
+ from shapely import get_parts, to_geojson
4
+ from shapely.geometry import MultiPolygon
5
+ from shapely.geometry.base import BaseGeometry
6
+ from shapely.geometry.polygon import orient
7
+
8
+ from .fetcher import fetch_node, fetch_relation, fetch_way
9
+
10
+
11
+ def right_hand_rule(geometry: BaseGeometry) -> BaseGeometry:
12
+ 'Enforces the "right hand rule" as defined by the GeoJSON spec (RFC-7946, Section 3.1.6).'
13
+ if geometry.geom_type == 'Polygon':
14
+ if not geometry.exterior.is_ccw:
15
+ return orient(geometry, sign=1)
16
+ return geometry
17
+ if geometry.geom_type == 'MultiPolygon':
18
+ return MultiPolygon([right_hand_rule(p) for p in geometry.geoms])
19
+ return geometry
20
+
21
+
22
+ def export_node(node_id) -> str:
23
+ if node := fetch_node(node_id):
24
+ return to_geojson(node)
25
+ return f'Error exporting node {node_id}.'
26
+
27
+
28
+ def export_way(way_id) -> str:
29
+ if way := fetch_way(way_id):
30
+ return to_geojson(right_hand_rule(way))
31
+ return f'Error exporting way {way_id}.'
32
+
33
+
34
+ def wrap_feature(part):
35
+ return {
36
+ 'type': 'Feature',
37
+ 'properties': {},
38
+ 'geometry': json.loads(to_geojson(right_hand_rule(part))),
39
+ }
40
+
41
+
42
+ def export_relation(relation_id) -> str:
43
+ if rel := fetch_relation(relation_id):
44
+ return json.dumps({'type': 'FeatureCollection', 'features': [wrap_feature(p) for p in get_parts(rel)]})
45
+ return f'Error exporting relation {relation_id}.'
@@ -0,0 +1,26 @@
1
+ 3-Clause BSD License
2
+
3
+ Copyright (c) 2015, Ronaldo "Racum" Ferreira
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ * Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+ * Neither the name of Ronaldo "Racum" Ferreira nor the
14
+ names of its contributors may be used to endorse or promote products
15
+ derived from this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL RONALDO "RACUM" FERREIRA BE LIABLE FOR ANY
21
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.1
2
+ Name: osmexp
3
+ Version: 0.1.0
4
+ Summary: OpenStreetMap GeoJSON Exporter
5
+ Home-page: https://github.com/racum/osmexp
6
+ Author: Ronaldo Ferreira
7
+ Author-email: ronaldo@racum.com
8
+ License: BSD-3-Clause
9
+ Project-URL: Source, https://github.com/racum/osmexp
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown; charset=UTF-8
18
+ License-File: LICENSE
19
+ Requires-Dist: requests
20
+ Requires-Dist: shapely
21
+
22
+ # osmexp - OpenStreetMap GeoJSON Exporter
23
+
24
+ The [OSM API](https://wiki.openstreetmap.org/wiki/API_v0.6) uses a specific format for their geometric primitive types based on nodes, ways and relations; and those unfortunately don’t translate 1:1 with [GeoJSON types](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1), thus a conversion is needed,
25
+
26
+ ## Installation:
27
+
28
+ Make sure have at least Python 3.11, and install it with:
29
+
30
+ ```
31
+ $ pip install osmexp
32
+ ```
33
+
34
+ The only requirements are `Requests` and `Shapely`.
35
+
36
+ ## Usage
37
+
38
+ For all exports, this is the format:
39
+
40
+ ```
41
+ $ osmexp [element_type] [element_id]
42
+ ```
43
+
44
+ Where `element_type` can be `node`, `way` or `rel` (for "relation"). And the ID is the numerical identifier used by OSM, if you don’t know how to get the OSM ID, [follow this article](https://racum.blog/articles/osm-to-geojson/#features-on-openstreetmap).
45
+
46
+ The output is set to `STDOUT`, and it is recommended to be piped into a file or another command.
47
+
48
+ ### Exporting Nodes:
49
+
50
+ ```
51
+ $ osmexp node 9589344640 > outdoor_seating.geojson
52
+ ```
53
+
54
+ This returns a GeoJSON with a `Point` as a root type.
55
+
56
+ ### Exporting Ways:
57
+
58
+ ```
59
+ $ osmexp way 398987317 > canal_grande.geojson
60
+ ```
61
+
62
+ This returns a GeoJSON with a `LineString` as a root type if the the line is “open” or `Polygon` if the line is “closed”.
63
+
64
+ ### Exporting Relations:
65
+
66
+ ```
67
+ $ osmexp rel 4817103 > venice.geojson
68
+ ```
69
+
70
+ This returns a GeoJSON with a `FeatureCollection` as a root, and the internal features as `LineString` or `Polygon`, depending if they can self-close, or fail the polygon transformation by other means (self crossing, etc).
71
+
72
+ ## License
73
+
74
+ This library is released under the **3-Clause BSD License**.
75
+
76
+ **tl;dr**: *"free to use as long as you credit me"*.
@@ -0,0 +1,10 @@
1
+ osmexp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ osmexp/__main__.py,sha256=RYSrs39Rr7fsQGIuGBuiRltw79Vy3F3E_qgZff1T9xE,1046
3
+ osmexp/fetcher.py,sha256=LJ5LLUm1DaHigJGwY9IsMvwaTi4_AoJE3beL0IBRS_8,1526
4
+ osmexp/geojson.py,sha256=MFpYErc5qchHH15YtnvT2wolmBs_LWLmy9wcyPqv3zQ,1418
5
+ osmexp-0.1.0.dist-info/LICENSE,sha256=vY_J4si56jceKgBxnf5EENVfNVQ4vdXeb9h3BCR-3Is,1544
6
+ osmexp-0.1.0.dist-info/METADATA,sha256=u06mp2Fnn_2HlIWftxsCmrmNe0re1-RmS3gNtGsq3Zc,2433
7
+ osmexp-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
8
+ osmexp-0.1.0.dist-info/entry_points.txt,sha256=AGuz3edRWLmTiyA8RI1dw28Jt10QO57nguXtbRgp7Ns,48
9
+ osmexp-0.1.0.dist-info/top_level.txt,sha256=P2nAZvg0o-tTlV4-PuQ7nBP2PcEEk3DXRq9wSyn2rQM,7
10
+ osmexp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ osmexp = osmexp.__main__:main
@@ -0,0 +1 @@
1
+ osmexp