Automated edits/StopConsuming StartProducing
This page documents planned automated edits by StopConsuming_StartProducing (Klaus: thunderbird (odd) office-dateien (even) de)
First documented mechanical edit of Titsa bus stops
(This mechanical update is intended to be a rehearsal for further and potentially larger mechanical edits.)
On Tenerife (island), bus stops used by the local operator Titsa (https://titsa.com) are tagged on OSM with "operator" = "Titsa" and a "ref" number identical to the bus stop number used internal by Titsa (e.g. "ref" = "1326" in the following example), see example: https://www.openstreetmap.org/node/1386852429
This internal ref number is for example visible in the route information of Titsa: https://titsa.com/index.php/tus-guaguas/lineas-y-horarios/linea-105 and it can be used to query the upcoming connections for that bus stop: https://titsa.com/movil/infoparada.php?IdParada=1326
However, some of these Titsa bus stops on Tenerife are outdated and not used anymore by Titsa. Therefore, I would like to tag them "disused:highway" and remove any "route_ref" tags using a Python script.
Intended update:
- "highway" = "bus_stop" ⇒ "disabled:highway" = "bus_stop"
- Remove "route_ref" if existent
Preparations
Before the next step I did some manual cleaning up, like identifying potential Titsa stops not tagged properly, tagging the operator properly (TITSA => Titsa), and such.
1) Identifying all Titsa bus stop on OSM:
// https://overpass-turbo.eu // Find all bus stops on Tenerife that have a 'ref' key [out:json][timeout:25]; // get Tenerife area by name area["name"="Tenerife"]["boundary"="political"]->.searchArea; // fetch nodes/ways/relations with the POI tag and the required key ( node["highway"="bus_stop"]["operator" = "Titsa"]["ref"](area.searchArea); ); out center;
Result: ≈3,940
2) Checking whether these are used by Titsa (using the ref number):
https://titsa.com/ajax/xGetInfoParada.php?id_parada=$i
Result (not used by Titsa): 114
3) Checking these 114 again, but a different website of Titsa:
https://titsa.com/movil/infoparada.php?IdParada=$i
Result (not used by Titsa): 46
4) Checking whether they are still part of any relation (using the node ids):
https://api.openstreetmap.org/api/0.6/node/$i/relations.json
Result: 13
5) Treating these relation dependencies manually:
- Deleted relations manually: 1
- Removed nodes (i.e. bus stop) from relation (i.e. bus route) based on Titsa website (https://titsa.com/index.php/tus-guaguas/lineas-y-horarios/linea-021): 11
- Corrected nodes manually (e.g. not part of Titsa) and removed from further consideration: 1
Result: 13
6) Nodes left to handle: 45
| Nodes | |
| node id | ref |
|---|---|
| 1250629292 | 1126 |
| 1250629190 | 1127 |
| 1250629279 | 1129 |
| 2191330802 | 1130 |
| 1250629288 | 1131 |
| 1250629347 | 1147 |
| 2191330801 | 1148 |
| 1250629265 | 1149 |
| 1250629377 | 1150 |
| 2242072687 | 1286 |
| 1395275024 | 1441 |
| 1797908858 | 1902 |
| 2137750735 | 1961 |
| 5313665896 | 2338 |
| 2191329178 | 2580 |
| 2191329100 | 2607 |
| 2242072538 | 2633 |
| 2242072812 | 2634 |
| 2242072274 | 2637 |
| 2242072280 | 2641 |
| 2456067873 | 2694 |
| 2456067862 | 2695 |
| 2456067894 | 2696 |
| 2456067885 | 2697 |
| 5313665897 | 2725 |
| 1223181177 | 4090 |
| 2209280948 | 4175 |
| 2209281056 | 4433 |
| 2209280868 | 4467 |
| 2209281055 | 4489 |
| 2209280286 | 4849 |
| 2209280361 | 4933 |
| 1223181147 | 5038 |
| 2209280283 | 5058 |
| 13653409061 | 5262 |
| 13706439970 | 5278 |
| 2198901536 | 7080 |
| 309748720 | 7280 |
| 4799055093 | 8196 |
| 1975543653 | 8313 |
| 12861185999 | 8403 |
| 2191331292 | 9186 |
| 940041154 | 9311 |
| 2191331277 | 9399 |
| 2210188861 | 9954 |
Proposed script and execution
| Script |
import requests, re
import xml.etree.ElementTree as ET
# Configuration
ACCESS_TOKEN = "..."
# Node IDs to modify
NODE_IDS = [4365362145,4365362146]
# Tags to remove completely
TAGS_TO_REMOVE = ["route_ref"]
# OSM API endpoints
#API_BASE = 'https://api.openstreetmap.org/api/0.6'
API_BASE = 'https://master.apis.dev.openstreetmap.org/api/0.6'
def get_node_data(node_id):
"""Fetch current node data from OSM"""
headers = {'Authorization': f'Bearer {ACCESS_TOKEN}'}
response = requests.get(
f'{API_BASE}/node/{node_id}',
headers=headers
)
response.raise_for_status()
return response.text
def modify_node_xml(node_xml, changeset_id):
"""
Modify node XML:
- Change 'highway' tag to 'disused:highway'
- Remove specified tags
"""
root = ET.fromstring(node_xml)
node = root.find('node')
if node is None:
raise ValueError("No node found in response")
# Update changeset ID
node.set('changeset', str(changeset_id))
# Process existing tags
tags_to_delete = []
for tag in node.findall('tag'):
key = tag.get('k')
# Change highway to disused:highway
if key == 'highway':
tag.set('k', 'disused:highway')
# Mark tags for deletion
if key in TAGS_TO_REMOVE:
tags_to_delete.append(tag)
# Remove marked tags
for tag in tags_to_delete:
node.remove(tag)
# Convert back to string
xml_str = ET.tostring(root, encoding='unicode')
return xml_str
def create_changeset(comment):
"""Create a changeset for the edits"""
headers = {'Authorization': f'Bearer {ACCESS_TOKEN}'}
changeset_xml = f'''<osm><changeset>
<tag k="osm_wiki_documentation_page" v=""/>
<tag k="discussion_before_edits" v=""/>
<tag k="mechanical" v="yes"/>
<tag k="created_by" v="https://wiki.openstreetmap.org/wiki/API_v0.6"/>
<tag k="created_by_library" v="osm_tag_disused_busstops.py"/>
<tag k="comment" v="Changed highway to disused:highway and removed obsolete tags"/>
</changeset></osm>'''
response = requests.put(
f'{API_BASE}/changeset/create',
data=changeset_xml,
headers=headers
)
response.raise_for_status()
return int(response.text)
def close_changeset(changeset_id):
"""Close the changeset"""
headers = {'Authorization': f'Bearer {ACCESS_TOKEN}'}
response = requests.put(
f'{API_BASE}/changeset/{changeset_id}/close',
headers=headers
)
response.raise_for_status()
def update_node(node_id, node_xml):
"""Update existing node using PUT with node ID"""
headers = {'Authorization': f'Bearer {ACCESS_TOKEN}'}
response = requests.put(f'{API_BASE}/node/{node_id}', data=node_xml, headers=headers)
response.raise_for_status()
return response.text
def main():
print("OpenStreetMap Bulk Edit Script")
print("=" * 50)
print(f"Nodes to modify: {NODE_IDS}")
print(f"Tags to remove: {TAGS_TO_REMOVE}")
print()
# Create changeset
print("Step 1: Creating changeset...")
changeset_id = create_changeset()
print(f"✓ Changeset created: {changeset_id}\n")
# Process each node
print("Step 2: Modifying nodes...")
modified_count = 0
for node_id in NODE_IDS:
try:
print(f" Processing node {node_id}...", end=" ")
# Get current node data
node_xml = get_node_data(node_id)
# Modify the XML
modified_xml = modify_node_xml(node_xml, changeset_id)
# Update node
update_node(node_id, modified_xml)
print("✓")
modified_count += 1
except Exception as e:
print(f"✗ Error: {e}")
# Close changeset
print(f"\nStep 3: Closing changeset...")
close_changeset(changeset_id)
print(f"✓ Changeset {changeset_id} closed\n")
print("=" * 50)
print(f"Successfully modified {modified_count}/{len(NODE_IDS)} nodes")
print(f"Changeset: {re.findall('https?://[^/]+', API_BASE)[0]}/changeset/{changeset_id}")
if __name__ == '__main__':
main()
|
Successful test runs (on https://master.apis.dev.openstreetmap.org):
- Created two bus stops manually, one with route_ref: https://master.apis.dev.openstreetmap.org/changeset/624751
- Applied the above script: https://master.apis.dev.openstreetmap.org/changeset/624752
- They are now "disabled:highway" = "bus_stop", and any "route_ref" is removed.
Intended execution:
- Execute in 1 bulk
Execution
n/a