Automated edits/StopConsuming StartProducing
This page documents planned automated edits by StopConsuming_StartProducing (Klaus: thunderbird (odd) office-dateien (even) de)
Also read: https://wiki.openstreetmap.org/wiki/Automated_Edits_code_of_conduct
First documented automated edit of TITSA bus stops
(This automated update is intended to be a rehearsal for further and potentially larger automated edits.)
Explanation of Geo situation
On Tenerife (island), bus stops used by the local operator Transportes Interurbanos de Tenerife (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-418 and it can be used to query the upcoming connections for that bus stop: https://titsa.com/movil/infoparada.php?IdParada=7140 (The latter website also includes the GPS coordinates of the stop in its source.) The almost identical website is also available under the following link: https://movil.titsa.com/infotransbordo.php?IdParada=1326
On the an official open data portal of Tenerife, you can also download the current detailed bus stop and route situation: https://datos.tenerife.es/es/datos/conjuntos-de-datos/informacion-sobre-el-sistema-de-transporte-de-titsa-en-tenerife They refer to the bus stop time table using the following link: http://www.titsa.com/correspondencias.php?idc=7140 (Also here you can find the GPS coordinates of the stop in the HTML source code.)
Furthermore, the TITSA website uses the following AJAX link to query in place bus stop information (incl. GPS): https://titsa.com/ajax/xGetInfoParada.php?id_parada=7140
Cause of and reason for automated edit
Cause
Some of these TITSA bus stops on Tenerife are outdated on OSM 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
Reason
Running a rehearsal for a larger automated edit.
Preparations
Before this I did some manual cleaning up on OSM, like identifying potential TITSA stops not tagged properly with an operator, tagging the operator properly (TITSA => Titsa), and such.
Finding outdated bus stops (round 1)
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"]["ref"](area.searchArea); ); out skel;
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 |
Finding outdated bus stops (round 2)
- Again queried Overpass to identify nodes with ref on Tenerife.
- Removed nodes with non-TITSA operator.
- Imported current GTFS bus stop list: https://datos.tenerife.es/es/datos/conjuntos-de-datos/informacion-sobre-el-sistema-de-transporte-de-titsa-en-tenerife (ZIP => stops.txt)
- Identified 58 nodes not in the GTFS list. (Ignored the Los Cristianos stops with "-".)
- Compared these 58 against the previously identified 45. Result: Of the 58, 19 where not identified previously. Of the 45, 6 are not part of the nodes not in the GTFS list.
- It seems the AJAX feedback "{"success":true,"parada":false,"lineas":null}" is not sufficient to reject any nodes.
- Taking the nodes not in the GTFS list AND identified in the first round => 39.
Nodes left to handle: 39
| Nodes | |
| node id | ref |
|---|---|
| 1250629292 | 1126 |
| 1250629190 | 1127 |
| 1250629279 | 1129 |
| 2191330802 | 1130 |
| 1250629288 | 1131 |
| 1250629347 | 1147 |
| 2191330801 | 1148 |
| 1250629265 | 1149 |
| 1250629377 | 1150 |
| 2242072687 | 1286 |
| 1797908858 | 1902 |
| 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 |
| 1223181147 | 5038 |
| 2209280283 | 5058 |
| 13706439970 | 5278 |
| 309748720 | 7280 |
| 4799055093 | 8196 |
| 1975543653 | 8313 |
| 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 = [1250629292,
1250629190,
1250629279,
2191330802,
1250629288,
1250629347,
2191330801,
1250629265,
1250629377,
2242072687,
1797908858,
5313665896,
2191329178,
2191329100,
2242072538,
2242072812,
2242072274,
2242072280,
2456067873,
2456067862,
2456067894,
2456067885,
5313665897,
1223181177,
2209280948,
2209281056,
2209280868,
2209281055,
2209280286,
1223181147,
2209280283,
13706439970,
309748720,
4799055093,
1975543653,
2191331292,
940041154,
2191331277,
2210188861]
# 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():
"""Create a changeset for the edits"""
headers = {'Authorization': f'Bearer {ACCESS_TOKEN}'}
changeset_xml = f'''<osm><changeset>
<tag k="osm_wiki_documentation_page" v="https://wiki.openstreetmap.org/wiki/Automated_edits/StopConsuming_StartProducing#First_documented_automated_edit_of_TITSA_bus_stops"/>
<tag k="discussion_before_edits" v="yes"/>
<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
Date/time: 2026 / 06 / 06 - 21:30 CEST (UTC+2)
| Log |
OpenStreetMap Bulk Edit Script
==================================================
Nodes to modify: [1250629292, 1250629190, 1250629279, 2191330802, 1250629288, 1250629347, 2191330801, 1250629265, 1250629377, 2242072687, 1797908858, 5313665896, 2191329178, 2191329100, 2242072538, 2242072812, 2242072274, 2242072280, 2456067873, 2456067862, 2456067894, 2456067885, 5313665897, 1223181177, 2209280948, 2209281056, 2209280868, 2209281055, 2209280286, 1223181147, 2209280283, 13706439970, 309748720, 4799055093, 1975543653, 2191331292, 940041154, 2191331277, 2210188861]
Tags to remove: ['route_ref']
Step 1: Creating changeset...
✓ Changeset created: 183753265
Step 2: Modifying nodes...
Processing node 1250629292... ✓
Processing node 1250629190... ✓
Processing node 1250629279... ✓
Processing node 2191330802... ✓
Processing node 1250629288... ✓
Processing node 1250629347... ✓
Processing node 2191330801... ✓
Processing node 1250629265... ✓
Processing node 1250629377... ✓
Processing node 2242072687... ✓
Processing node 1797908858... ✓
Processing node 5313665896... ✓
Processing node 2191329178... ✓
Processing node 2191329100... ✓
Processing node 2242072538... ✓
Processing node 2242072812... ✓
Processing node 2242072274... ✓
Processing node 2242072280... ✓
Processing node 2456067873... ✓
Processing node 2456067862... ✓
Processing node 2456067894... ✓
Processing node 2456067885... ✓
Processing node 5313665897... ✓
Processing node 1223181177... ✓
Processing node 2209280948... ✓
Processing node 2209281056... ✓
Processing node 2209280868... ✓
Processing node 2209281055... ✓
Processing node 2209280286... ✓
Processing node 1223181147... ✓
Processing node 2209280283... ✓
Processing node 13706439970... ✓
Processing node 309748720... ✓
Processing node 4799055093... ✗ Error: 410 Client Error: Gone for url: https://api.openstreetmap.org/api/0.6/node/4799055093
Processing node 1975543653... ✗ Error: 410 Client Error: Gone for url: https://api.openstreetmap.org/api/0.6/node/1975543653
Processing node 2191331292... ✓
Processing node 940041154... ✓
Processing node 2191331277... ✓
Processing node 2210188861... ✓
Step 3: Closing changeset...
✓ Changeset 183753265 closed
==================================================
Successfully modified 37/39 nodes
Changeset: https://api.openstreetmap.org/changeset/183753265
|
Changeset: https://api.openstreetmap.org/changeset/183753265
Result: Success ✓
(Corrected nodes 4799055093 and https://www.openstreetmap.org/node/1975543653 1975543653] manually: https://www.openstreetmap.org/changeset/183754054 & https://www.openstreetmap.org/changeset/183754162)
Addition of missing TITSA bus stops
(This 2nd automated update is intended to be another rehearsal for further and potentially larger automated edits.)
Explanation of Geo situation
See above: #Explanation of Geo situation.
Cause of and reason for automated edit
Cause
According to the GTFS information (https://datos.tenerife.es/es/datos/conjuntos-de-datos/informacion-sobre-el-sistema-de-transporte-de-titsa-en-tenerife), there do exist bus stops in the TITSA network not yet on OSM.
Intended update:
- Addition of bus stops with relevant ref number
- Making sure there is not already a bus stop nearby which can be used
Reason
Running a rehearsal for a larger automated edit.
Preparations
n/a