source: subversion/applications/utils/change_tags/change_tags.py @ 13032

Last change on this file since 13032 was 13032, checked in by crschmidt, 11 years ago

Tool for changing tags on a large number of objects at once.

File size: 11.7 KB
Line 
1#!/usr/bin/python
2
3"""change_tags.py, an OSM tool.
4
5   This application is designed to convert tags, based on a downloaded .osm
6   file. To use, open the .py file and edit the contents of the 'converter'
7   function to change the tags in the way you want to, then comment out the
8   'converter = False' line immediately after the function body. Once you have
9   done that, Typically, usage would be:
10
11       python tag_changer.py --dry-run -f file.osm --verbose
12
13   followed by  manual inspection of the output XML. At the end, it will report
14   how many total nodes/elements there are, and how many would be changed by
15   the conversion.
16
17   Once you've done that, call:
18
19       python tag_changer.py -f file.osm -u <username> -p <password>
20
21   This should iterate through and perform the updates against the API,
22   reporting skipped nodes or errors at the end.
23
24   If you are working only with your own data, you should usually use
25   
26       --only-mine displayName
27
28   Which will skip any updates which do not have you as the last author.
29"""   
30
31__author__  = "Christopher Schmidt <crschmidt@crschmidt.net>"
32__version__ = "0.2"
33__revision__ = "$Id$"
34
35import dbm
36import sys, re, xml.sax
37from xml.sax.handler import ContentHandler
38
39requires = []
40
41try:
42    from xml.etree.cElementTree import Element, SubElement, tostring
43except ImportError:
44    requires.append("Requires xml.etree.cElementTree. Try Python2.5?")
45
46try:
47    import httplib2
48except ImportError, E:
49    requires.append("No httplib2! Try easy_install httplib2 (%s)" % E)
50
51def converter(tags, type):
52    """Pass in tags dict and object type. Function must return True (object
53    changed, upload to server) or False (nothing changed, don't reupload).
54    Change the tags dict in place.
55   
56    By default, this function simply returns 'False' (nothing changed);
57    however, it has some examples of what can be done as alternatives.
58
59    This is the function you should edit in order to change the tags you
60    wish to change.
61
62    By default, adds a created_by tag identifying the source of the change.
63    """
64
65    return False 
66   
67    changed = False
68   
69    if type == "node":
70        return False 
71   
72    # Fix a typo
73    if 'name' in tags and 'Playgroung' in tags['name']:
74        tags['name'] = tags['name'].replace("Playgroung", "Playground")
75        changed = True   
76   
77    # adjust a tag based on a name
78    if 'name' in tags and ('park' in tags['name'].lower() or
79       'playground' in tags['name'].lower()):
80        tags['leisure'] = 'park'
81        changed = True
82   
83    # change a key in a tag
84    if 'leisure' in tags and tags['leisure'] == 'recreation_ground':
85        del tags['leisure']
86        if not 'landuse' in tags:
87            tags['landuse'] = 'recreation_ground'
88        else:
89            tags['landuse'] = "%s; recreation_ground" % tags['landuse']
90        changed = True
91
92    tags['created_by'] = 'change_tags.py %s' % __version__
93
94    return changed   
95
96# Comment this line out once you have edited the converter above.
97converter = False
98
99#### DO NOT CHANGE CODE AFTER THIS LINE #####
100
101
102def indent(elem, level=0):
103    """Used for pretty printing XML."""
104    i = "\n" + level*"  "
105    if len(elem):
106        if not elem.text or not elem.text.strip():
107            elem.text = i + "  "
108        for e in elem:
109            indent(e, level+1)
110            if not e.tail or not e.tail.strip():
111                e.tail = i + "  "
112        if not e.tail or not e.tail.strip():
113            e.tail = i
114    else:
115        if level and (not elem.tail or not elem.tail.strip()):
116            elem.tail = i
117
118class changeTags (ContentHandler):
119   
120    def __init__ (self, converter, db=None, user=None, password=None, dry_run=None, noisy_errors=None, only_mine=None, verbose=None):
121        ContentHandler.__init__(self)
122       
123        self.converter = converter
124        self.user = user
125        self.password = password
126        self.dry_run = dry_run
127        self.noisy_errors = noisy_errors
128        self.verbose = verbose
129        self.only_mine = only_mine
130        self.db = db
131
132        self.changes = {'node': 0, 'way': 0} 
133        self.already_changed = {'node': 0, 'way': 0} 
134        self.read = {'node': 0, 'way': 0, 'tag': 0} 
135        self.errors = []
136        self.skipped = []
137       
138        if not self.dry_run:
139            if self.user and self.password:
140                self.h = httplib2.Http()
141                self.h.add_credentials(self.user, self.password)
142            else:
143                raise Exception("Username and password required.")
144
145    def upload(self, xml):
146        if self.only_mine and self.current['user'] != self.only_mine:
147            self.skipped.append({'id': self.current['id'], 'type':self.current['type'], 'reason': "User %s doesn't match." % self.current['user']})
148            return
149        db_key = "%s:%s" % (self.current['type'], self.current['id']) 
150        if self.db and self.db.has_key(db_key):
151            self.already_changed[self.current['type']] += 1
152            return
153        url = "http://api.openstreetmap.org/api/0.5/%s/%s" % (self.current['type'], self.current['id'])
154        if self.dry_run:
155            if self.verbose:
156                print "URL:  %s" % url
157                print "XML:\n%s" % xml
158            self.changes[self.current['type']] += 1
159        else:
160            try:
161                if self.verbose: 
162                    print "Opening URL %s" % url
163                resp, content = self.h.request(url, "PUT", body=xml)
164                if resp.status != '200':
165                    error = {'item': self.current, 'code': resp.status, 'data': content}
166                    if self.noisy_errors: print "Error occurred! %s" % error
167                    self.errors.append(error)
168                else:
169                    self.db[db_key] = "1" 
170                    self.changes[self.current['type']] += 1
171
172            except Exception, E:
173                error = {'item': self.current, 'code': -1, 'data': str(E)}
174                if self.noisy_errors: print "Error occurred! %s" % error
175                self.errors.append(error)
176
177    def startElement (self, name, attr):
178        if name == 'node':
179            self.current = {'type': 'node', 'id': attr['id'],
180                'lon':attr["lon"], 'lat':attr["lat"], 'tags': {}}
181        elif name == 'way':
182            self.current = {'type': 'way', 'id': attr['id'], 'nodes':[], 'tags': {}}
183        elif name =='nd' and self.current:
184            self.current['nodes'].append(attr["ref"])
185        elif name == 'tag' and self.current:
186            self.current['tags'][attr['k']] = attr['v']
187        if 'user' in attr and self.current:
188            self.current['user'] = attr['user']
189       
190        if name in ['node', 'way', 'tag']:
191            self.read[name] += 1
192
193    def endElement (self, name):
194       
195        if name == 'way':
196            new_tags = converter(self.current['tags'], 'way')
197            if new_tags:
198                osm = Element('osm', {'version': '0.5'})
199
200                parent = SubElement(osm, 'way', {'id': self.current['id']})
201                for n in self.current['nodes']:
202                    SubElement(parent, "nd", {'ref': n})
203                keys = self.current['tags'].keys()
204                keys.sort()
205                for key in keys:
206                    SubElement(parent, "tag", {'k': key, 'v': self.current['tags'][key]})
207                indent(osm)
208                self.upload(tostring(osm))
209       
210        elif name == 'node':
211            new_tags = converter(self.current['tags'], 'node')
212            if new_tags:
213                osm = Element('osm', {'version': '0.5'})
214
215                parent = SubElement(osm, 'node', {'id': self.current['id'], 'lat': self.current['lat'], 'lon': self.current['lon']})
216                keys = self.current['tags'].keys()
217                keys.sort()
218                for key in keys:
219                    SubElement(parent, "tag", {'k': key, 'v': self.current['tags'][key]})
220               
221                indent(osm)
222                self.upload(tostring(osm))
223
224
225if __name__ == "__main__":
226    if not converter:
227        if requires:
228            __doc__ = "%s\nRequired dependancies unavailable: \n %s" % (__doc__, ("\n  ".join(requires)))
229        print __doc__
230        sys.exit(1)
231   
232    if requires:
233        print "Required dependancies unavailable: \n  " % ("\n  ".join(requires)) 
234        sys.exit(2)
235
236    from optparse import OptionParser
237    parser = OptionParser()
238
239    parser.add_option("-f", "--file", help="source file. default is stdin", dest="file")
240    parser.add_option("-d", "--dry-run", action="store_true", default=False, help="print URLs and XML for changed items, rather than actually changing them.", dest="dry_run")
241    parser.add_option("-u", "--username", help="username for OSM API", dest="username")
242    parser.add_option("-p", "--password", help="api password (will prompt if not provided and required)", dest="password")
243    parser.add_option("-e", "--noisy-errors", dest="noisy_errors", default=False, action="store_true")
244    parser.add_option("--verbose", help="be verbose", dest="verbose", default=False, action="store_true")
245    parser.add_option('-o', "--only-mine", dest="only_mine", help="Provide a username/displayname which will be used to check if an edit should be perormed.") 
246    parser.add_option("-n", "--no-status", dest="no_status", action="store_true", default=False, help="Don't store status db for recovery of upload (faster; riskier")
247    parser.add_option('--profile', dest='profile', action="store_true", help="Report profiler stats", default=False)
248
249    options, args = parser.parse_args()
250
251    f = None
252    if options.file:
253        f = open(options.file)
254        if not options.no_status:
255            db = dbm.open("%s.db" % options.file, "c")
256        else:
257            db = None 
258    else:
259        f = sys.stdin
260        db=None
261        if not options.no_status:
262            print "Using stdin, unable to create status db"
263    if not options.dry_run and options.username:
264        import getpass
265        options.password = getpass.getpass("Password: ") 
266
267    osmParser = changeTags(
268            converter,
269            db=db,
270            user=options.username, 
271            password=options.password, 
272            dry_run=options.dry_run, 
273            noisy_errors = options.noisy_errors, 
274            only_mine=options.only_mine,
275            verbose=options.verbose)
276   
277    prof = None
278    if options.profile:
279        import hotshot, hotshot.stats
280        prof = hotshot.Profile("tagChanger.prof")
281   
282    try:
283        if prof:
284            prof.runcall(xml.sax.parse, f, osmParser)
285        else:   
286            xml.sax.parse( f, osmParser )
287    except KeyboardInterrupt:
288        print "\nStopping at %s %s due to interrupt"  % (osmParser.current['type'], osmParser.current['id'])
289        pass
290    except Exception, E:
291        print "\nStopping at %s %s due to exception: \n%s"  % (osmParser.current['type'], osmParser.current['id'], E)
292
293    print "Total Read: %s nodes, %s ways, %s tags"  % (osmParser.read['node'], osmParser.read['way'], osmParser.read['tag'])
294    print "Total Changed: %s nodes, %s ways"  % (osmParser.changes['node'], osmParser.changes['way'])
295    print "Previously Changed: %s nodes, %s ways"  % (osmParser.already_changed['node'], osmParser.already_changed['way'])
296    if len(osmParser.errors):
297        print "The following %s errors occurred:" % len(osmParser.errors)
298    for e in osmParser.errors:
299         print "%s: %s. Code: %s. Error Text: %s" % (e['item']['type'],e['item']['id'], e['code'], e['data'])
300    if len(osmParser.skipped):
301        print "The following %s items were skipped:" % len(osmParser.skipped)
302    for e in osmParser.skipped:
303         print "%s: %s. Reason: %s" % (e['type'],e['id'], e['reason'])
304    if prof:
305        stats = hotshot.stats.load("tagChanger.prof")
306        stats.strip_dirs()
307        stats.sort_stats('time', 'calls')
308        stats.print_stats(20)
Note: See TracBrowser for help on using the repository browser.