Skip to content

Replace upload_osm with export_osm #7

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

Merged
merged 3 commits into from
Mar 24, 2025
Merged
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
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ repos:
rev: v2.3.0
hooks:
- id: codespell
exclude: CODE_OF_CONDUCT.md|demo/|src/osm_ai_helper/upload_osm.py
exclude: CODE_OF_CONDUCT.md|demo/
args: [--ignore-regex, nd]
42 changes: 16 additions & 26 deletions demo/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
from pathlib import Path
from shutil import move

Expand All @@ -11,7 +10,7 @@
from streamlit_folium import st_folium

from osm_ai_helper.run_inference import run_inference
from osm_ai_helper.upload_osm import upload_osm
from osm_ai_helper.export_osm import convert_polygons


@st.fragment
Expand Down Expand Up @@ -90,34 +89,25 @@ def handle_polygon(polygon):


@st.fragment
def upload_results(output_path):
def download_results(output_path):
st.divider()
st.header("Upload all polygons in `keep`")
st.header("Export Results")

st.markdown(
"The results will be uploaded using the [osm-ai-helper](https://www.openstreetmap.org/user/osm-ai-helper) profile."
"The results will be exported in [OsmChange](https://wiki.openstreetmap.org/wiki/OsmChange) format."
"\nYou can then import the file in [any of the supported editors](https://wiki.openstreetmap.org/wiki/OsmChange#Editors) format."
)
st.markdown(
"You can check the [Colab Notebook](ttps://colab.research.google.com/github/mozilla-ai/osm-ai-helper/blob/main/demo/run_inference_point.ipynb)"
" and the [Authorization Guide](https://mozilla-ai.github.io/osm-ai-helper/authorization)"
" to contribute with your own OpenStreetMap account."

changeset = convert_polygons(
results_dir=output_path / "keep",
tags={"leisure": "swimming_pool", "access": "private", "location": "outdoor"},
)
st.download_button(
label="Download all polygons in `keep`",
data=changeset,
file_name="exported_results.osc",
mime="type/xml",
)
contributor = st.text_input("(Optional) Indicate your name for attribution")
if st.button("Upload all polygons in `keep`"):
if contributor:
comment = f"Add Swimming Pools. Contributed by {contributor}"
else:
comment = "Add Swimming Pools"

changeset = upload_osm(
results_dir=output_path / "keep",
client_id=os.environ["OSM_CLIENT_ID"],
client_secret=os.environ["OSM_CLIENT_SECRET"],
comment=comment,
)
st.success(
f"Changeset created: https://www.openstreetmap.org/changeset/{changeset}"
)


st.title("OpenStreetMap AI Helper")
Expand Down Expand Up @@ -171,6 +161,6 @@ def upload_results(output_path):
for new in Path(output_path).glob("*.json"):
handle_polygon(new)

upload_results(output_path)
download_results(output_path)
else:
st.warning("No `new` swimming pools were found. Try a different location.")
33 changes: 11 additions & 22 deletions demo/run_inference_area.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -352,18 +352,16 @@
"id": "z6QrI0Rz-x7s"
},
"source": [
"# Upload Results"
"# Export Results"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "I5WUDmU1Cod_"
},
"outputs": [],
"cell_type": "markdown",
"metadata": {},
"source": [
"len(list(Path(\"keep\").glob(\"*.json\")))"
"The results will be exported in [OsmChange](https://wiki.openstreetmap.org/wiki/OsmChange) format.\n",
"\n",
"You can then import the file in [any of the supported editors](https://wiki.openstreetmap.org/wiki/OsmChange#Editors) format."
]
},
{
Expand All @@ -374,7 +372,7 @@
},
"outputs": [],
"source": [
"from osm_ai_helper.upload_osm import upload_osm"
"from osm_ai_helper.export_osm import export_osm"
]
},
{
Expand All @@ -385,21 +383,12 @@
},
"outputs": [],
"source": [
"changeset = upload_osm(\n",
" \"keep\", os.environ[\"OSM_CLIENT_ID\"], os.environ[\"OSM_CLIENT_SECRET\"]\n",
"export_osm(\n",
" results_dir=output_path / \"keep\",\n",
" output_dir=\"exported\",\n",
" tags={\"leisure\": \"swimming_pool\", \"access\": \"private\", \"location\": \"outdoor\"},\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "J4qsnxTVBM38"
},
"outputs": [],
"source": [
"print(f\"Changeset created: https://www.openstreetmap.org/changeset/{changeset}\")"
]
}
],
"metadata": {
Expand Down
50 changes: 18 additions & 32 deletions demo/run_inference_point.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"metadata": {
"id": "2E1XhnrQT4X1"
},
Expand All @@ -86,9 +86,7 @@
"\n",
"from google.colab import userdata\n",
"\n",
"os.environ[\"MAPBOX_TOKEN\"] = userdata.get(\"MAPBOX_TOKEN\")\n",
"os.environ[\"OSM_CLIENT_ID\"] = userdata.get(\"OSM_CLIENT_ID\")\n",
"os.environ[\"OSM_CLIENT_SECRET\"] = userdata.get(\"OSM_CLIENT_SECRET\")"
"os.environ[\"MAPBOX_TOKEN\"] = userdata.get(\"MAPBOX_TOKEN\")"
]
},
{
Expand Down Expand Up @@ -540,7 +538,16 @@
"id": "z6QrI0Rz-x7s"
},
"source": [
"# Upload Results"
"# Export Results"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The results will be exported in [OsmChange](https://wiki.openstreetmap.org/wiki/OsmChange) format.\n",
"\n",
"You can then import the file in [any of the supported editors](https://wiki.openstreetmap.org/wiki/OsmChange#Editors) format."
]
},
{
Expand All @@ -551,7 +558,7 @@
},
"outputs": [],
"source": [
"from osm_ai_helper.upload_osm import upload_osm"
"from osm_ai_helper.export_osm import export_osm"
]
},
{
Expand All @@ -564,34 +571,13 @@
"id": "Abc-2nTB9RsR",
"outputId": "0927b590-47df-4fcd-b21c-96e8aa7b3cb4"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"\u001b[32m2025-02-19 10:20:31.996\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mosm_ai_helper.upload_osm\u001b[0m:\u001b[36mload_token\u001b[0m:\u001b[36m36\u001b[0m - \u001b[1mToken loaded from /tmp/osm_token.json\u001b[0m\n",
"\u001b[32m2025-02-19 10:20:31.997\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mosm_ai_helper.upload_osm\u001b[0m:\u001b[36mensure_authorized_session\u001b[0m:\u001b[36m75\u001b[0m - \u001b[1mToken exists, attempting to use it.\u001b[0m\n",
"\u001b[32m2025-02-19 10:20:32.634\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mosm_ai_helper.upload_osm\u001b[0m:\u001b[36mopen_changeset\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mCREATE: <Response [200]>, b'162694284'\u001b[0m\n",
"\u001b[32m2025-02-19 10:20:32.965\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mosm_ai_helper.upload_osm\u001b[0m:\u001b[36mupload_polygon\u001b[0m:\u001b[36m161\u001b[0m - \u001b[1mUPLOAD: <Response [200]>, b'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<diffResult version=\"0.6\" generator=\"openstreetmap-cgimap 2.0.1 (3288087 spike-08.openstreetmap.org)\" copyright=\"OpenStreetMap and contributors\" attribution=\"http://www.openstreetmap.org/copyright\" license=\"http://opendatacommons.org/licenses/odbl/1-0/\">\\n <node old_id=\"-1\" new_id=\"12600349038\" new_version=\"1\"/>\\n <node old_id=\"-2\" new_id=\"12600349039\" new_version=\"1\"/>\\n <node old_id=\"-3\" new_id=\"12600349040\" new_version=\"1\"/>\\n <node old_id=\"-4\" new_id=\"12600349041\" new_version=\"1\"/>\\n <node old_id=\"-5\" new_id=\"12600349042\" new_version=\"1\"/>\\n <node old_id=\"-6\" new_id=\"12600349043\" new_version=\"1\"/>\\n <way old_id=\"-1\" new_id=\"1361116496\" new_version=\"1\"/>\\n</diffResult>\\n'\u001b[0m\n",
"\u001b[32m2025-02-19 10:20:33.169\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mosm_ai_helper.upload_osm\u001b[0m:\u001b[36mopen_changeset\u001b[0m:\u001b[36m121\u001b[0m - \u001b[1mCLOSE: <Response [200]>, b''\u001b[0m\n"
]
}
],
"source": [
"changeset = upload_osm(\n",
" output_path / \"keep\", os.environ[\"OSM_CLIENT_ID\"], os.environ[\"OSM_CLIENT_SECRET\"]\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "J4qsnxTVBM38"
},
"outputs": [],
"source": [
"print(f\"Changeset created: https://www.openstreetmap.org/changeset/{changeset}\")"
"export_osm(\n",
" results_dir=output_path / \"keep\",\n",
" output_dir=\"exported\",\n",
" tags={\"leisure\": \"swimming_pool\", \"access\": \"private\", \"location\": \"outdoor\"},\n",
")"
]
}
],
Expand Down
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

::: osm_ai_helper.run_inference

::: osm_ai_helper.upload_osm
::: osm_ai_helper.export_osm

::: osm_ai_helper.utils.inference

Expand Down
12 changes: 0 additions & 12 deletions docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ You need to:
- Create an account: https://console.mapbox.com/
- Follow this guide to obtain your [Default Public Token](https://docs.mapbox.com/help/getting-started/access-tokens/#your-default-public-token).

## `OSM_CLIENT_ID` and `OSM_CLIENT_SECRET`

Used to upload the results after [running inference](https://colab.research.google.com/github/mozilla-ai/osm-ai-helper/blob/main/demo/run_inference_point.ipynb) to the OpenStreetMap database.

You need to:

- Create an account: https://www.openstreetmap.org/user/new
- Register a new OAuth2 application: https://www.openstreetmap.org/oauth2/applications/new
Grant `Modify the map (write_api)`.
Set the redirect URL to `https://127.0.0.1:8000`.
- Copy and save the `Client ID` and `Client Secret`.

## `HF_TOKEN`

Only needed if you are [Creating a Dataset](https://colab.research.google.com/github/mozilla-ai/osm-ai-helper/blob/main/demo/create_dataset.ipynb) and/or [Finetuning a Model](https://colab.research.google.com/github/mozilla-ai/osm-ai-helper/blob/main/demo/finetune_model.ipynb) in order to upload the results to the [HuggingFace Hub](https://huggingface.co/docs/hub/index).
Expand Down
File renamed without changes
10 changes: 6 additions & 4 deletions docs/step-by-step-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,21 @@ Based on overlap, all the polygons are categorized into `existing` (green), `new
The really relevant ones are the `new` (yellow), the others just serve as reference on how the model behaves
for polygons already existing in OpenStreetMap.

## **Step 4: Review, filter and upload the `new` polygons to OpenStreetMap**
## **Step 4: Review, filter and export the `new` polygons**

The `new` polygons can be manually reviewed and filtered:

![Filter Polygons](./images/filter-polygons.png)

The ones you chose to `keep` will be uploaded to OpenStreetMap using [`upload_osm`](api.md/#osm_ai_helper.upload_osm.upload_osm).
The ones you chose to `keep` will be exportedin [OsmChange](https://wiki.openstreetmap.org/wiki/OsmChange) format.

You can then import the file in [any of the supported editors](https://wiki.openstreetmap.org/wiki/OsmChange#Editors) format.

!!! warning

Once uploaded, the changes will be added to OpenStreetMap, so ensure you're confident about the ones you've kept.
Make sure to carefully review and edit any predicted polygon.

![Polygon Uploaded](./images/polygon-uploaded.png)
![Exported Polygons](./images/polygon-exported.png)

## 🎨 **Customizing the Blueprint**

Expand Down
93 changes: 93 additions & 0 deletions src/osm_ai_helper/export_osm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import json
import xml.etree.ElementTree as ET
from pathlib import Path

from fire import Fire
from loguru import logger


MAX_ELEMENTS_PER_OSMCHANGE = 50


def convert_polygons(lon_lat_polygons: list[list[float]], tags: dict | None = None):
osmchange = ET.Element(
"osmChange",
version="0.6",
generator="https://github.com/mozilla-ai/osm-ai-helper",
)
create = ET.SubElement(osmchange, "create")

n_nodes = 0
n_ways = 0
ways = []
for lon_lat_polygon in lon_lat_polygons:
n_ways += 1
way = ET.Element("way", id=f"-{n_ways}", version="0")

# Predicted polygons always contains a duplicate of the first point
lon_lat_polygon.pop()

first_node = n_nodes + 1
for lon, lat in lon_lat_polygon:
n_nodes += 1
ET.SubElement(
create,
"node",
id=f"-{n_nodes}",
lon=f"{lon}",
lat=f"{lat}",
version="0",
)
ET.SubElement(way, "nd", ref=f"-{n_nodes}")

# OSM requires to duplicate first point to close the polygon
ET.SubElement(way, "nd", ref=f"-{first_node}")

if tags:
for k, v in tags.items():
ET.SubElement(way, "tag", k=k, v=v)

# ways need to be added as subelements only every node from every polygon has been added
ways.append(way)

for way in ways:
create.append(way)

ET.SubElement(osmchange, "modify")
delete = ET.SubElement(osmchange, "delete")
delete.set("if-unused", "true")
return osmchange


@logger.catch(reraise=True)
def export_osm(results_dir: str, output_dir: str, tags: dict = None) -> None:
"""
Export the polygons in `results_dir` to an [`.osc`](https://wiki.openstreetmap.org/wiki/OsmChange) file.

Args:
results_dir (str): Directory containing the results.
The results should be in the format of `*.json` files.
See [`run_inference`][osm_ai_helper.run_inference.run_inference].
"""
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True, parents=True)

lon_lat_polygons = [
json.loads(result.read_text()) for result in Path(results_dir).glob("*.json")
]
logger.info(f"Converting {len(lon_lat_polygons)} polygons to OsmChange format.")
logger.info(
f"Each set of {MAX_ELEMENTS_PER_OSMCHANGE} polygons will be saved to a separate file."
)
for n_polygon in range(0, len(lon_lat_polygons), MAX_ELEMENTS_PER_OSMCHANGE):
to_be_converted = lon_lat_polygons[
n_polygon : n_polygon + MAX_ELEMENTS_PER_OSMCHANGE
]
osmchange = convert_polygons(to_be_converted, tags)
output_file = f"{n_polygon}-{n_polygon + MAX_ELEMENTS_PER_OSMCHANGE}.osc"
logger.info(f"Writing OsmChange to {output_file}")
(output_dir / output_file).write_bytes(ET.tostring(osmchange, "utf-8"))


if __name__ == "__main__":
Fire(export_osm)
Loading