osmexp 0.2.0__tar.gz
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-0.2.0/LICENSE +26 -0
- osmexp-0.2.0/PKG-INFO +104 -0
- osmexp-0.2.0/README.md +78 -0
- osmexp-0.2.0/osmexp/__init__.py +0 -0
- osmexp-0.2.0/osmexp/__main__.py +32 -0
- osmexp-0.2.0/osmexp/fetcher.py +87 -0
- osmexp-0.2.0/osmexp/geojson.py +55 -0
- osmexp-0.2.0/osmexp.egg-info/PKG-INFO +104 -0
- osmexp-0.2.0/osmexp.egg-info/SOURCES.txt +15 -0
- osmexp-0.2.0/osmexp.egg-info/dependency_links.txt +1 -0
- osmexp-0.2.0/osmexp.egg-info/entry_points.txt +2 -0
- osmexp-0.2.0/osmexp.egg-info/requires.txt +8 -0
- osmexp-0.2.0/osmexp.egg-info/top_level.txt +1 -0
- osmexp-0.2.0/pyproject.toml +59 -0
- osmexp-0.2.0/setup.cfg +4 -0
- osmexp-0.2.0/tests/test_fetcher.py +52 -0
- osmexp-0.2.0/tests/test_geojson.py +48 -0
osmexp-0.2.0/LICENSE
ADDED
|
@@ -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.
|
osmexp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: osmexp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: OpenStreetMap GeoJSON Exporter
|
|
5
|
+
Author-email: Ronaldo Ferreira <ronaldo@racum.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Source, https://github.com/racum/osmexp
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Requires-Dist: shapely
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-recording; extra == "dev"
|
|
23
|
+
Requires-Dist: vcrpy; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# osmexp - OpenStreetMap GeoJSON Exporter
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## Installation:
|
|
32
|
+
|
|
33
|
+
Make sure you have at least Python 3.11, and install it with:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
$ pip install osmexp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The only requirements are `Requests` and `Shapely`.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
For all exports, this is the format:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
$ osmexp [element_type] [element_id]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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).
|
|
50
|
+
|
|
51
|
+
The output is set to `STDOUT`, and it is recommended to be piped into a file or another command. On failure, an error message is sent to `STDERR` and the process exits with a non-zero status.
|
|
52
|
+
|
|
53
|
+
### Exporting Nodes:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
$ osmexp node 9589344640 > outdoor_seating.geojson
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This returns a GeoJSON with a `Point` as a root type.
|
|
60
|
+
|
|
61
|
+
### Exporting Ways:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ osmexp way 398987317 > canal_grande.geojson
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This returns a GeoJSON with a `LineString` as a root type if the line is “open” or `Polygon` if the line is “closed”.
|
|
68
|
+
|
|
69
|
+
### Exporting Relations:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ osmexp rel 4817103 > venice.geojson
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
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).
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
Clone the repository and install in editable mode with the `dev` extras (tests, linter):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
$ pip install -e '.[dev]'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
All dependencies are declared in `pyproject.toml`; there are no separate requirements files.
|
|
86
|
+
|
|
87
|
+
Run the test suite (it replays recorded HTTP responses, so no network is needed):
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
$ pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Lint and format with [Ruff](https://docs.astral.sh/ruff/):
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
$ ruff check .
|
|
97
|
+
$ ruff format .
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
This library is released under the **3-Clause BSD License**.
|
|
103
|
+
|
|
104
|
+
**tl;dr**: *"free to use as long as you credit me"*.
|
osmexp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# osmexp - OpenStreetMap GeoJSON Exporter
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Installation:
|
|
6
|
+
|
|
7
|
+
Make sure you have at least Python 3.11, and install it with:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
$ pip install osmexp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The only requirements are `Requests` and `Shapely`.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
For all exports, this is the format:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
$ osmexp [element_type] [element_id]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
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).
|
|
24
|
+
|
|
25
|
+
The output is set to `STDOUT`, and it is recommended to be piped into a file or another command. On failure, an error message is sent to `STDERR` and the process exits with a non-zero status.
|
|
26
|
+
|
|
27
|
+
### Exporting Nodes:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
$ osmexp node 9589344640 > outdoor_seating.geojson
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This returns a GeoJSON with a `Point` as a root type.
|
|
34
|
+
|
|
35
|
+
### Exporting Ways:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
$ osmexp way 398987317 > canal_grande.geojson
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This returns a GeoJSON with a `LineString` as a root type if the line is “open” or `Polygon` if the line is “closed”.
|
|
42
|
+
|
|
43
|
+
### Exporting Relations:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
$ osmexp rel 4817103 > venice.geojson
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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).
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
Clone the repository and install in editable mode with the `dev` extras (tests, linter):
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
$ pip install -e '.[dev]'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
All dependencies are declared in `pyproject.toml`; there are no separate requirements files.
|
|
60
|
+
|
|
61
|
+
Run the test suite (it replays recorded HTTP responses, so no network is needed):
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ pytest
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Lint and format with [Ruff](https://docs.astral.sh/ruff/):
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ ruff check .
|
|
71
|
+
$ ruff format .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
This library is released under the **3-Clause BSD License**.
|
|
77
|
+
|
|
78
|
+
**tl;dr**: *"free to use as long as you credit me"*.
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .geojson import ExportError, export_node, export_relation, export_way
|
|
7
|
+
|
|
8
|
+
EXPORTERS = {
|
|
9
|
+
'node': export_node,
|
|
10
|
+
'way': export_way,
|
|
11
|
+
'rel': export_relation,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog='osmexp',
|
|
18
|
+
description='Export an OpenStreetMap element as GeoJSON to STDOUT.',
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument('element', choices=EXPORTERS, help='The OSM element type.')
|
|
21
|
+
parser.add_argument('id', type=int, help='The OSM element ID (an integer).')
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
print(EXPORTERS[args.element](args.id))
|
|
26
|
+
except ExportError as error:
|
|
27
|
+
print(error, file=sys.stderr)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == '__main__':
|
|
32
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from shapely import get_parts, polygonize_full
|
|
5
|
+
from shapely.geometry import GeometryCollection, LineString, Point, Polygon
|
|
6
|
+
|
|
7
|
+
API_URL = 'https://api.openstreetmap.org/api/0.6'
|
|
8
|
+
USER_AGENT = 'osmexp/0.2.0 (+https://github.com/racum/osmexp)'
|
|
9
|
+
TIMEOUT = 30
|
|
10
|
+
MIN_LINESTRING_NODES = 2
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fetch_elements(path: str) -> list[dict] | None:
|
|
14
|
+
"Fetches an OSM API endpoint and returns its `elements`, or `None` on any failure."
|
|
15
|
+
try:
|
|
16
|
+
response = requests.get(
|
|
17
|
+
f'{API_URL}/{path}',
|
|
18
|
+
headers={'User-Agent': USER_AGENT},
|
|
19
|
+
timeout=TIMEOUT,
|
|
20
|
+
)
|
|
21
|
+
response.raise_for_status()
|
|
22
|
+
elements = response.json().get('elements')
|
|
23
|
+
except (requests.RequestException, ValueError):
|
|
24
|
+
return None
|
|
25
|
+
return elements or None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_geometries(elements: list[dict]) -> tuple[dict[int, Point], dict[int, LineString]]:
|
|
29
|
+
"Builds Shapely geometries keyed by OSM id: nodes as `Point`, ways as `LineString`."
|
|
30
|
+
nodes = {e['id']: Point(e['lon'], e['lat']) for e in elements if e['type'] == 'node'}
|
|
31
|
+
ways = {
|
|
32
|
+
e['id']: LineString([nodes[n] for n in e['nodes']])
|
|
33
|
+
for e in elements
|
|
34
|
+
if e['type'] == 'way' and len(e['nodes']) >= MIN_LINESTRING_NODES
|
|
35
|
+
}
|
|
36
|
+
return nodes, ways
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fetch_node(node_id: int) -> Point | None:
|
|
40
|
+
elements = fetch_elements(f'node/{node_id}.json')
|
|
41
|
+
if elements is None:
|
|
42
|
+
return None
|
|
43
|
+
nodes, _ = parse_geometries(elements)
|
|
44
|
+
return next(iter(nodes.values()), None)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fetch_way(way_id: int) -> LineString | Polygon | None:
|
|
48
|
+
elements = fetch_elements(f'way/{way_id}/full.json')
|
|
49
|
+
if elements is None:
|
|
50
|
+
return None
|
|
51
|
+
_, ways = parse_geometries(elements)
|
|
52
|
+
way = next(iter(ways.values()), None)
|
|
53
|
+
if way is None:
|
|
54
|
+
return None
|
|
55
|
+
return Polygon(way) if way.is_ring else way
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def fetch_relation(relation_id: int) -> GeometryCollection | None:
|
|
59
|
+
elements = fetch_elements(f'relation/{relation_id}/full.json')
|
|
60
|
+
if elements is None:
|
|
61
|
+
return None
|
|
62
|
+
_, ways = parse_geometries(elements)
|
|
63
|
+
relation = next((e for e in elements if e['type'] == 'relation'), None)
|
|
64
|
+
if relation is None:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
outer, inner = [], []
|
|
68
|
+
for member in relation['members']:
|
|
69
|
+
if member['type'] != 'way':
|
|
70
|
+
continue
|
|
71
|
+
line = ways.get(member['ref'])
|
|
72
|
+
if line is None:
|
|
73
|
+
continue
|
|
74
|
+
(inner if member['role'] == 'inner' else outer).append(line)
|
|
75
|
+
|
|
76
|
+
outer_polygons, *outer_leftovers = polygonize_full(outer)
|
|
77
|
+
inner_rings = [p.exterior for p in get_parts(polygonize_full(inner)[0])]
|
|
78
|
+
|
|
79
|
+
parts = []
|
|
80
|
+
for polygon in get_parts(outer_polygons):
|
|
81
|
+
holes = [ring for ring in inner_rings if polygon.contains(ring)]
|
|
82
|
+
parts.append(Polygon(polygon.exterior, holes) if holes else polygon)
|
|
83
|
+
|
|
84
|
+
# Ways that could not be assembled into polygons (e.g. self-crossing) stay as lines.
|
|
85
|
+
parts.extend(itertools.chain.from_iterable(get_parts(leftover) for leftover in outer_leftovers))
|
|
86
|
+
|
|
87
|
+
return GeometryCollection(parts)
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
class ExportError(Exception):
|
|
12
|
+
"Raised when an OSM element cannot be fetched or exported."
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def right_hand_rule(geometry: BaseGeometry) -> BaseGeometry:
|
|
16
|
+
'Enforces the "right hand rule" as defined by the GeoJSON spec (RFC-7946, Section 3.1.6).'
|
|
17
|
+
if geometry.geom_type == 'Polygon':
|
|
18
|
+
return orient(geometry, sign=1)
|
|
19
|
+
if geometry.geom_type == 'MultiPolygon':
|
|
20
|
+
return MultiPolygon([right_hand_rule(p) for p in geometry.geoms])
|
|
21
|
+
return geometry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def export_node(node_id: int) -> str:
|
|
25
|
+
node = fetch_node(node_id)
|
|
26
|
+
if node is None:
|
|
27
|
+
raise ExportError(f'Could not export node {node_id}.')
|
|
28
|
+
return to_geojson(node)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def export_way(way_id: int) -> str:
|
|
32
|
+
way = fetch_way(way_id)
|
|
33
|
+
if way is None:
|
|
34
|
+
raise ExportError(f'Could not export way {way_id}.')
|
|
35
|
+
return to_geojson(right_hand_rule(way))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def wrap_feature(part: BaseGeometry) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
'type': 'Feature',
|
|
41
|
+
'properties': {},
|
|
42
|
+
'geometry': json.loads(to_geojson(right_hand_rule(part))),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def export_relation(relation_id: int) -> str:
|
|
47
|
+
relation = fetch_relation(relation_id)
|
|
48
|
+
if relation is None:
|
|
49
|
+
raise ExportError(f'Could not export relation {relation_id}.')
|
|
50
|
+
return json.dumps(
|
|
51
|
+
{
|
|
52
|
+
'type': 'FeatureCollection',
|
|
53
|
+
'features': [wrap_feature(p) for p in get_parts(relation)],
|
|
54
|
+
}
|
|
55
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: osmexp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: OpenStreetMap GeoJSON Exporter
|
|
5
|
+
Author-email: Ronaldo Ferreira <ronaldo@racum.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Source, https://github.com/racum/osmexp
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Requires-Dist: shapely
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-recording; extra == "dev"
|
|
23
|
+
Requires-Dist: vcrpy; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# osmexp - OpenStreetMap GeoJSON Exporter
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## Installation:
|
|
32
|
+
|
|
33
|
+
Make sure you have at least Python 3.11, and install it with:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
$ pip install osmexp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The only requirements are `Requests` and `Shapely`.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
For all exports, this is the format:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
$ osmexp [element_type] [element_id]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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).
|
|
50
|
+
|
|
51
|
+
The output is set to `STDOUT`, and it is recommended to be piped into a file or another command. On failure, an error message is sent to `STDERR` and the process exits with a non-zero status.
|
|
52
|
+
|
|
53
|
+
### Exporting Nodes:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
$ osmexp node 9589344640 > outdoor_seating.geojson
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This returns a GeoJSON with a `Point` as a root type.
|
|
60
|
+
|
|
61
|
+
### Exporting Ways:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ osmexp way 398987317 > canal_grande.geojson
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This returns a GeoJSON with a `LineString` as a root type if the line is “open” or `Polygon` if the line is “closed”.
|
|
68
|
+
|
|
69
|
+
### Exporting Relations:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ osmexp rel 4817103 > venice.geojson
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
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).
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
Clone the repository and install in editable mode with the `dev` extras (tests, linter):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
$ pip install -e '.[dev]'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
All dependencies are declared in `pyproject.toml`; there are no separate requirements files.
|
|
86
|
+
|
|
87
|
+
Run the test suite (it replays recorded HTTP responses, so no network is needed):
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
$ pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Lint and format with [Ruff](https://docs.astral.sh/ruff/):
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
$ ruff check .
|
|
97
|
+
$ ruff format .
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
This library is released under the **3-Clause BSD License**.
|
|
103
|
+
|
|
104
|
+
**tl;dr**: *"free to use as long as you credit me"*.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
osmexp/__init__.py
|
|
5
|
+
osmexp/__main__.py
|
|
6
|
+
osmexp/fetcher.py
|
|
7
|
+
osmexp/geojson.py
|
|
8
|
+
osmexp.egg-info/PKG-INFO
|
|
9
|
+
osmexp.egg-info/SOURCES.txt
|
|
10
|
+
osmexp.egg-info/dependency_links.txt
|
|
11
|
+
osmexp.egg-info/entry_points.txt
|
|
12
|
+
osmexp.egg-info/requires.txt
|
|
13
|
+
osmexp.egg-info/top_level.txt
|
|
14
|
+
tests/test_fetcher.py
|
|
15
|
+
tests/test_geojson.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
osmexp
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "osmexp"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "OpenStreetMap GeoJSON Exporter"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Ronaldo Ferreira", email = "ronaldo@racum.com" }]
|
|
13
|
+
dependencies = ["requests", "shapely"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Natural Language :: English",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Source = "https://github.com/racum/osmexp"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest", "pytest-recording", "vcrpy", "ruff"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
osmexp = "osmexp.__main__:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
packages = ["osmexp"]
|
|
35
|
+
|
|
36
|
+
[tool.black]
|
|
37
|
+
line-length = 120
|
|
38
|
+
target-version = ['py311']
|
|
39
|
+
include = '\.pyi?$'
|
|
40
|
+
skip-string-normalization = true
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
target-version = "py311"
|
|
44
|
+
line-length = 120
|
|
45
|
+
lint.select = ["E", "F", "TID", "I", "B", "PGH", "UP", "A", "C4", "PIE", "T20", "RSE", "RET", "SIM", "ARG", "PTH", "ERA", "PL", "RUF"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.format]
|
|
48
|
+
quote-style = "single"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint.per-file-ignores]
|
|
51
|
+
"tests/*" = ["PLR2004"]
|
|
52
|
+
"osmexp/__main__.py" = ["PLR2004", "T201"]
|
|
53
|
+
|
|
54
|
+
[[tool.mypy.overrides]]
|
|
55
|
+
module = "shapely.*"
|
|
56
|
+
ignore_missing_imports = true
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
addopts = "--record-mode=none"
|
osmexp-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from osmexp.fetcher import fetch_node, fetch_relation, fetch_way
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.vcr()
|
|
7
|
+
def test_fetch_node_as_point():
|
|
8
|
+
node = fetch_node(9589344640) # Outdoor seating.
|
|
9
|
+
assert node.geom_type == 'Point'
|
|
10
|
+
assert (node.x, node.y) == (12.3394685, 45.4331611)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.vcr()
|
|
14
|
+
def test_fetch_open_way_as_linestring():
|
|
15
|
+
way = fetch_way(398987317) # Canal Grande.
|
|
16
|
+
assert way.geom_type == 'LineString'
|
|
17
|
+
assert len(way.coords) == 125
|
|
18
|
+
assert way.length == pytest.approx(0.049109547915808335) # Point units, not meters.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.vcr()
|
|
22
|
+
def test_fetch_closed_way_as_polygon():
|
|
23
|
+
way = fetch_way(430963095) # Column of Saint Mark.
|
|
24
|
+
assert way.geom_type == 'Polygon'
|
|
25
|
+
assert way.area == pytest.approx(3.089739999806592e-09) # Point units, not meters.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.vcr()
|
|
29
|
+
def test_fetch_single_relation_as_polygon():
|
|
30
|
+
rel = fetch_relation(4817103) # Venice.
|
|
31
|
+
assert rel.geom_type == 'GeometryCollection'
|
|
32
|
+
assert rel.geoms[0].geom_type == 'Polygon'
|
|
33
|
+
assert len(rel.geoms[0].exterior.coords) == 849
|
|
34
|
+
assert rel.geoms[0].area == pytest.approx(0.0006893646889149643) # Point units, not meters.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.vcr()
|
|
38
|
+
def test_fetch_split_relation_as_many_polygons():
|
|
39
|
+
rel = fetch_relation(1850539) # Parque Estadual Paulo César Vinha
|
|
40
|
+
assert rel.geom_type == 'GeometryCollection'
|
|
41
|
+
assert len(rel.geoms) == 9
|
|
42
|
+
assert {g.geom_type for g in rel.geoms} == {'Polygon'}
|
|
43
|
+
assert sum(g.area for g in rel.geoms) == pytest.approx(0.0013636596847249436)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.vcr()
|
|
47
|
+
def test_fetch_self_crossing_relation_as_many_linestrings():
|
|
48
|
+
rel = fetch_relation(284570) # Suzuka
|
|
49
|
+
assert rel.geom_type == 'GeometryCollection'
|
|
50
|
+
assert len(rel.geoms) == 2
|
|
51
|
+
assert {g.geom_type for g in rel.geoms} == {'LineString'}
|
|
52
|
+
assert sum(g.area for g in rel.geoms) == 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from osmexp.geojson import export_node, export_relation, export_way
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.vcr()
|
|
9
|
+
def test_export_node():
|
|
10
|
+
geojson = json.loads(export_node(9589344640))
|
|
11
|
+
assert geojson['type'] == 'Point'
|
|
12
|
+
assert geojson['coordinates'] == [12.3394685, 45.4331611]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.vcr()
|
|
16
|
+
def test_export_open_way():
|
|
17
|
+
geojson = json.loads(export_way(398987317))
|
|
18
|
+
assert geojson['type'] == 'LineString'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.vcr()
|
|
22
|
+
def test_export_closed_way():
|
|
23
|
+
geojson = json.loads(export_way(430963095))
|
|
24
|
+
assert geojson['type'] == 'Polygon'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.vcr()
|
|
28
|
+
def test_export_single_relation():
|
|
29
|
+
geojson = json.loads(export_relation(4817103))
|
|
30
|
+
assert geojson['type'] == 'FeatureCollection'
|
|
31
|
+
assert len(geojson['features']) == 1
|
|
32
|
+
assert {f['geometry']['type'] for f in geojson['features']} == {'Polygon'}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.vcr()
|
|
36
|
+
def test_export_split_relation():
|
|
37
|
+
geojson = json.loads(export_relation(1850539))
|
|
38
|
+
assert geojson['type'] == 'FeatureCollection'
|
|
39
|
+
assert len(geojson['features']) == 9
|
|
40
|
+
assert {f['geometry']['type'] for f in geojson['features']} == {'Polygon'}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.vcr()
|
|
44
|
+
def test_export_self_crossing_relation():
|
|
45
|
+
geojson = json.loads(export_relation(284570))
|
|
46
|
+
assert geojson['type'] == 'FeatureCollection'
|
|
47
|
+
assert len(geojson['features']) == 2
|
|
48
|
+
assert {f['geometry']['type'] for f in geojson['features']} == {'LineString'}
|