198 lines
4.9 KiB
Python
198 lines
4.9 KiB
Python
|
#!/usr/bin/env python3
|
||
|
|
||
|
import sys
|
||
|
import json
|
||
|
import time
|
||
|
import concurrent.futures
|
||
|
import requests
|
||
|
from collections import defaultdict
|
||
|
from ipo import ipo, opi, p
|
||
|
|
||
|
|
||
|
OSM_USERNAME = ""
|
||
|
|
||
|
if not OSM_USERNAME:
|
||
|
print("Fill in your OSM username in the script first.", file=sys.stderr)
|
||
|
sys.exit(1)
|
||
|
|
||
|
|
||
|
session = requests.Session()
|
||
|
|
||
|
|
||
|
changes = defaultdict(dict)
|
||
|
for oneway_bicycle in ("yes", "no", "-1", "other"):
|
||
|
for cycleway_both in ("yes", "no", "other"):
|
||
|
changes \
|
||
|
[f"removed oneway:bicycle={oneway_bicycle}"] \
|
||
|
[f"added cycleway:both={cycleway_both}"] \
|
||
|
= []
|
||
|
changes \
|
||
|
[f"removed oneway:bicycle={oneway_bicycle}"] \
|
||
|
["didn't modify cycleway:both"] \
|
||
|
= []
|
||
|
|
||
|
oneway_added_after_cycleway_both = []
|
||
|
nothing_to_report = []
|
||
|
preconditions_not_met = []
|
||
|
|
||
|
|
||
|
def normalized_oneway(value: str) -> str:
|
||
|
if value in ("yes", "1", "true"):
|
||
|
return "yes"
|
||
|
elif value in ("no", "0", "false"):
|
||
|
return "no"
|
||
|
elif value in ("-1", "reverse"):
|
||
|
return "-1"
|
||
|
else:
|
||
|
return "other"
|
||
|
|
||
|
def normalized_cycleway(value: str) -> str:
|
||
|
if value in ("yes", "no"):
|
||
|
return value
|
||
|
else:
|
||
|
return "other"
|
||
|
|
||
|
|
||
|
def get(url):
|
||
|
r = session.get(
|
||
|
url, timeout=30,
|
||
|
headers={
|
||
|
"User-Agent": "M!dgard's script to detect data corruption by a Vespucci bug, run by "
|
||
|
"`{OSM_USERNAME}`",
|
||
|
"X-Note": "See https://community.openstreetmap.org/t/98353. This script is used on "
|
||
|
"output of Overpass to check the tag history to determine if a road was corrupted "
|
||
|
f"by a Vespucci bug or UX issue. Run by OSM user `{OSM_USERNAME}`, contact them in "
|
||
|
"case of problems."
|
||
|
}
|
||
|
)
|
||
|
if r.status_code == 503:
|
||
|
time.sleep(60)
|
||
|
return get(url)
|
||
|
r.raise_for_status()
|
||
|
return r
|
||
|
|
||
|
|
||
|
def handle_way(i, total, way_id):
|
||
|
print(f"\r{i}/{len(ways)}", flush=True, end="", file=sys.stderr)
|
||
|
|
||
|
history_url = f"https://api.openstreetmap.org/api/0.6/way/{way_id}/history.json"
|
||
|
r = get(history_url)
|
||
|
|
||
|
history = (ipo(r.json()["elements"]) | reversed | list | opi)
|
||
|
# history = [{"version": 6, …}, {"version": 5, …}, …]
|
||
|
|
||
|
sort_in = None
|
||
|
|
||
|
if (
|
||
|
history[0].get("tags", {}).get("cycleway:both") != "no" or
|
||
|
history[0].get("tags", {}).get("oneway") not in ("yes", "-1") or
|
||
|
"oneway:bicycle" in history[0].get("tags", {})
|
||
|
):
|
||
|
print(
|
||
|
f"w{way_id}: preconditions for tags not met: wrong query used, "
|
||
|
"or OSM element modified after executing query",
|
||
|
file=sys.stderr, flush=True
|
||
|
)
|
||
|
sort_in = preconditions_not_met
|
||
|
sort_in.append(way_id)
|
||
|
return
|
||
|
|
||
|
for previous_version, version in zip(history[1:], history):
|
||
|
assert sort_in is None
|
||
|
|
||
|
cs = version["changeset"]
|
||
|
v = version["version"]
|
||
|
version_prefix = f"w{way_id}: v{v} (#{cs})"
|
||
|
|
||
|
|
||
|
if not previous_version.get("tags", []):
|
||
|
sort_in = nothing_to_report
|
||
|
break
|
||
|
|
||
|
|
||
|
assert "oneway:bicycle" not in version["tags"]
|
||
|
oneway_bicycle_removed = "oneway:bicycle" in previous_version["tags"]
|
||
|
cycleway_both_modified = (
|
||
|
"cycleway:both" in version["tags"] and
|
||
|
previous_version["tags"].get("cycleway:both") != version["tags"]["cycleway:both"]
|
||
|
)
|
||
|
|
||
|
|
||
|
# oneway changed somewhere after the purported StreetComplete change
|
||
|
if previous_version["tags"].get("oneway") != version["tags"].get("oneway"):
|
||
|
print(
|
||
|
f"\n{version_prefix} +oneway={version['tags']['oneway']}",
|
||
|
flush=True, file=sys.stderr
|
||
|
)
|
||
|
sort_in = oneway_added_after_cycleway_both
|
||
|
break
|
||
|
|
||
|
|
||
|
oneway_bicycle = previous_version["tags"].get("oneway:bicycle")
|
||
|
cycleway_both = version["tags"].get("cycleway:both")
|
||
|
|
||
|
if cycleway_both_modified:
|
||
|
|
||
|
if oneway_bicycle_removed:
|
||
|
|
||
|
print(
|
||
|
f"\n{version_prefix} "
|
||
|
f"+cycleway:both={cycleway_both} -oneway:bicycle={oneway_bicycle}",
|
||
|
flush=True, file=sys.stderr
|
||
|
)
|
||
|
sort_in = changes \
|
||
|
[f"removed oneway:bicycle={normalized_oneway(oneway_bicycle)}"] \
|
||
|
[f"added cycleway:both={normalized_cycleway(cycleway_both)}"]
|
||
|
break
|
||
|
|
||
|
else:
|
||
|
sort_in = nothing_to_report
|
||
|
break
|
||
|
|
||
|
if oneway_bicycle_removed:
|
||
|
# oneway:bicycle=* was removed after cycleway:both was added
|
||
|
print(
|
||
|
f"\n{version_prefix} "
|
||
|
f"-oneway:bicycle={oneway_bicycle}",
|
||
|
flush=True, file=sys.stderr
|
||
|
)
|
||
|
sort_in = changes \
|
||
|
[f"removed oneway:bicycle={normalized_oneway(oneway_bicycle)}"] \
|
||
|
["didn't modify cycleway:both"]
|
||
|
break
|
||
|
|
||
|
if sort_in is None:
|
||
|
sort_in = nothing_to_report
|
||
|
|
||
|
sort_in.append(way_id)
|
||
|
|
||
|
|
||
|
print(
|
||
|
"Feed JSON data from this Overpass wizard query on stdin:\n"
|
||
|
"(cycleway:both=no and (oneway=yes or oneway=1 or oneway=true or oneway=-1 or oneway=reverse) "
|
||
|
"and oneway:bicycle!=*) and type:way global\n",
|
||
|
file=sys.stderr
|
||
|
)
|
||
|
|
||
|
ways = json.load(sys.stdin)["elements"]
|
||
|
for way in ways:
|
||
|
assert way["type"] == "way"
|
||
|
|
||
|
try:
|
||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
||
|
_ = list(executor.map(
|
||
|
lambda i_way: handle_way(i_way[0], len(ways), i_way[1]),
|
||
|
enumerate(map(lambda x: x["id"], ways))
|
||
|
))
|
||
|
|
||
|
finally:
|
||
|
print(json.dumps(
|
||
|
{
|
||
|
"changes": changes,
|
||
|
"oneway_added_after_cycleway_both": oneway_added_after_cycleway_both,
|
||
|
"nothing_to_report": nothing_to_report,
|
||
|
"preconditions_not_met": preconditions_not_met
|
||
|
},
|
||
|
indent="\t"
|
||
|
))
|