Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WiP] Export results #66

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,17 @@ Helper

.. automodule:: overpy.helper
:members:


Format/Export
-------------

.. warning::

This feature is in pre alpha stage. Please report any issues.

.. automodule:: overpy.format.geojson
:members:

.. automodule:: overpy.format.osm_xml
:members:
Empty file added overpy/format/__init__.py
Empty file.
63 changes: 63 additions & 0 deletions overpy/format/geojson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import json
from typing import Any, Dict, Optional, TextIO

import overpy


def dump(result: overpy.Result, fp: TextIO, nodes: bool = False, ways: bool = False, json_args: Optional[dict] = None):
"""
Use the result from the Overpass API to generate GeoJSON.

More information:

* http://geojson.org/

:param result: The result from the Overpass API
:param fp: Filepointer to use
:param nodes: Export nodes
:param ways: Export ways
:param json_args: Additional arguments passed to json.dump(...)
:return:
"""
features = []
if nodes:
for node in result.nodes:
properties: Dict[str, Any] = {}
features.append({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
float(node.lon),
float(node.lat)
]
},
"properties": properties
})

if ways:
for way in result.ways:
properties = {}
coordinates = []
for node in way.nodes:
Copy link

@rowanwins rowanwins Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this excellent PR!
Just one brief suggestion - Some way's have duplicate first and last coordinates and so they ought to be treated as Polygon rather than LineString.
For more info check out https://wiki.openstreetmap.org/wiki/Way#Types_of_way
and
https://wiki.openstreetmap.org/wiki/Overpass_turbo/Polygon_Features (this info feels a bit more interpretable)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the information. I will have a look at the documentation.

coordinates.append([
float(node.lon),
float(node.lat)
])
features.append({
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": coordinates
},
"properties": properties
})

geojson = {
"type": "FeatureCollection",
"features": features
}

if json_args is None:
json_args = {}
json.dump(geojson, fp, **json_args)
111 changes: 111 additions & 0 deletions overpy/format/osm_xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from typing import TextIO
from xml.sax.saxutils import escape

import overpy


def dump(result: overpy.Result, fp: TextIO):
"""
Use the result from the Overpass API to generate OSM XML

More information:

* http://wiki.openstreetmap.org/wiki/OSM_XML

:param result: The result from the Overpass API
:param fp: Filepointer to use
:return:
"""
fp.write('<?xml version="1.0" encoding="UTF-8"?>\n')
fp.write('<osm version="0.6" generator="OverPy {0}">\n'.format(overpy.__version__))
lat_min = result.nodes[0].lat
lat_max = lat_min
lon_min = result.nodes[0].lon
lon_max = lon_min
for node in result.nodes:
if node.lat < lat_min:
lat_min = node.lat
elif node.lat > lat_max:
lat_max = node.lat

if node.lon < lon_min:
lon_min = node.lon
elif node.lon > lon_max:
lon_max = node.lon

fp.write(
'<bounds minlat="{0:f}" minlon="{1:f}" maxlat="{2:f}" maxlon="{3:f}"/>\n'.format(
lat_min,
lon_min,
lat_max,
lon_max
)
)

for node in result.nodes:
fp.write(
'<node id="{0:d}" lat="{1:f}" lon="{2:f}"'.format(
node.id,
node.lat,
node.lon
)
)
if len(node.tags) == 0:
fp.write('/>\n')
continue
fp.write('>\n')
for k, v in node.tags.items():
fp.write(
'<tag k="{0:s}" v="{1:s}"/>\n'.format(
escape(k),
escape(v)
)
)
fp.write('</node>\n')

for way in result.ways:
fp.write('<way id="{0:d}"'.format(way.id))
if len(way.nodes) == 0 and len(way.tags) == 0:
fp.write('/>\n')
continue
fp.write('>\n')
for node in way.nodes:
fp.write('<nd ref="{0:d}"/>\n'.format(node.id))

for k, v in way.tags.items():
fp.write(
'<tag k="{0:s}" v="{1:s}"/>\n'.format(
escape(k),
escape(v)
)
)

fp.write('</way>\n')

for relation in result.relations:
fp.write('<relation id="{0:d}'.format(relation.id))
if len(relation.tags) == 0 and len(relation.members) == 0:
fp.write('/>\n')

for member in relation.members:
if not isinstance(member, overpy.RelationMember):
continue
fp.write(
'<member type="{0:s}" ref="{1:d}" role="{2:s}"/>\n'.format(
member._type_value,
member.ref,
member.role
)
)

for k, v in relation.tags.items():
fp.write(
'<tag k="{0:s}" v="{1:s}"/>\n'.format(
escape(k),
escape(v)
)
)

fp.write('</relation>\n')

fp.write('</osm>')
22 changes: 22 additions & 0 deletions tests/test_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import overpy
from overpy.format import geojson, osm_xml

from tests import read_file

from io import StringIO


class TestGeoJSON(object):
def test_node01(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/node-01.json"))
fp = StringIO()
geojson.dump(result, fp, nodes=True, ways=True)


class TestOSMXML(object):
def test_node01(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/node-01.json"))
fp = StringIO()
osm_xml.dump(result, fp)