File: openstreetmap.py

package info (click to toggle)
pytrainer 2.2.1-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,924 kB
  • sloc: python: 15,677; perl: 8,361; xml: 195; makefile: 87; sh: 54
file content (373 lines) | stat: -rw-r--r-- 17,494 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
import os, stat
import sys
import logging
from gi.repository import Gtk

import string
from lxml import etree

import httplib, httplib2
import urllib2
import mimetools, mimetypes
from json import dumps, loads       #   for deserializing JSON data form javascript

from pytrainer.extensions.mapviewer import MapViewer
from pytrainer.extensions.osm import Osm
from pytrainer.lib.localization import gtk_str

class openstreetmap:
    def __init__(self, parent = None, pytrainer_main = None, conf_dir = None, options = None):
        self.parent = parent
        self.pytrainer_main = pytrainer_main
        self.options = options
        self.conf_dir = conf_dir
        self.description = " "
        self.tags = ""
        self.visibility = "private"
        self.mozTitle=""               # Keeps embedded mozilla document title while displaying map for private area selection
        self.privBounds=[]             # Bounds of areas to be anonymized 

    def run(self, id, activity=None):  #TODO Convert to use activity...
        logging.debug(">>")
        try:
            uri = "http://api.openstreetmap.org/api/0.6/gpx/create" #URI for uploading traces to OSM
            if 'username' not in self.options or self.options['username'] == "" or 'password' not in self.options or self.options['password'] == "":
                logging.error("Must have username and password configured")
                raise Exception("Must have username and password configured")
            username = self.options['username']
            password = self.options['password']
            gpx_file = "%s/gpx/%s.gpx" % (self.conf_dir, id)
            if not os.path.isfile(gpx_file):
                raise Exception(str(gps_file) + ' File not found')
            #GPX file is ok and found, so open it
            logging.debug("GPX file: %s found, size: %d" % (gpx_file, os.path.getsize(gpx_file)))
            f = open(gpx_file, 'r')
            file_contents = f.read()
            #TODO Fix to use etree functionality.....
            if file_contents.find("<?xml version='1.0' encoding='ASCII'?>") != -1:
                logging.debug("GPX file: %s has ASCII encoding - updating to UTF-8 for OSM support" % gpx_file)
                f.close()                   #Close readonly file
                f = open(gpx_file, 'w')     #and open file for writing
                file_contents = file_contents.replace("<?xml version='1.0' encoding='ASCII'?>","<?xml version='1.0' encoding='UTF-8'?>", 1)
                f.write(file_contents)      #Write new content
                f.close()                   #Close
                f = open(gpx_file, 'r')     #Reopen in readonly mode
            #Get extra info from user
            response=self.display_options_window()
            if not response==Gtk.ResponseType.ACCEPT:
                f.close()
                logging.debug("User abort")
                return
            if self.makeanon:
                logging.debug("User requested anonymizing of GPX data")
                f.close()                   #Close standard gpxfile
                gpx_file = self.make_gpx_private(gpx_file)
                f = open(gpx_file, 'r')     #Open anonymous gpxfile in readonly mode
            fields = (("description",self.description), ("tags",self.tags), ("visibility",self.visibility))
            logging.debug("Added fields: %s" % str(fields))
            #Multipart encode the request
            boundary, body = self.multipart_encode(fields=fields, files=(("file", f),))
            content_type = 'multipart/form-data; boundary=%s' % boundary
            #Finished with the file so close it
            f.close()
            #Add the http headers to the request
            h = httplib2.Http()
            headers = {
                'Content-Type': content_type
            }
            #Add basic authentication credentials to the request
            h.add_credentials(username, password)
            #Show user something is happening
            msg = _("Posting GPX trace to Openstreetmap\n\nPlease wait this could take several minutes")
            md = Gtk.MessageDialog(self.pytrainer_main.windowmain.window1, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, Gtk.ButtonsType.NONE, msg)
            md.set_title(_("Openstreetmap Extension Processing"))
            md.set_modal(True)
            md.show()
            while Gtk.events_pending(): # This allows the GUI to update
                Gtk.main_iteration()    # before completion of this entire action
            logging.debug("before request posting")
            #POST request to OSM
            res, content = h.request(uri, 'POST', body=body, headers=headers)
            logging.debug("after request posting")
            logging.debug("Got response status: %s, reason: %s, content: %s" % (res.status, res.reason, content))
            if res.reason == 'OK':
                res_msg = "Successfully posted to OSM.\nYou should get an email with the outcome of the upload soon\n\nTrace id is %s" % content
            else:
                res_msg = "Some error occured\nGot a status %s, reason %s\nContent was: %s" % (res.status, res.reason, content)
            #Close 'Please wait' dialog
            md.destroy()
            #Show the user the result
            md = Gtk.MessageDialog(self.pytrainer_main.windowmain.window1, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, res_msg)
            md.set_title(_("Openstreetmap Extension Upload Complete"))
            md.set_modal(False)
            md.run()
            md.destroy()
        except Exception as e:
                msg = _("Error while uploading file to OSM: " + str(e))
                md = Gtk.MessageDialog(self.pytrainer_main.windowmain.window1, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, msg)
                md.set_title(_("Openstreetmap Extension Error"))
                md.run()
                md.destroy()
                return
        finally:
            logging.debug("<<")

    def display_options_window(self):
        self.prefwindow = Gtk.Dialog(title=_("Please add any additional information for this upload"), parent=None, flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT, Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT))
        self.prefwindow.set_modal(False)
        table = Gtk.Table(1,3)
        self.entryList = []
        #Add description
        label = Gtk.Label(label="<b>Description</b>")
        label.set_use_markup(True)
        entry = Gtk.Entry()
        self.entryList.append(entry)
        table.attach(label,0,1,0,1)
        table.attach(entry,1,2,0,1)
        #Add tags
        label = Gtk.Label(label="<b>Tags</b>")
        label.set_use_markup(True)
        entry = Gtk.Entry()
        self.entryList.append(entry)
        table.attach(label,0,1,1,2)
        table.attach(entry,1,2,1,2)
        #Add visibility
        label = Gtk.Label(label="<b>Visibility</b>")
        label.set_use_markup(True)
        combobox = Gtk.ComboBoxText()
        combobox.append_text("private")
        combobox.append_text("public")
        combobox.append_text("trackable")
        combobox.append_text("identifiable")
        combobox.set_active(0)
        table.attach(combobox,1,2,2,3)
        self.entryList.append(combobox)
        table.attach(label,0,1,2,3)
        #Add anonymize GPX option
        label = Gtk.Label(label="<b>Anonymize GPX Data</b>")
        label.set_use_markup(True)
        table.attach(label,0,1,3,4)
        checkbutton = Gtk.CheckButton()
        table.attach(checkbutton,1,2,3,4)
        self.entryList.append(checkbutton)
        #Add anon area selection button
        button = Gtk.Button("Area selection")
        button.connect("clicked",self.areaSelect)
        table.attach(button,1,2,4,5)
        self.entryList.append(button)
        #Build dialog and show
        self.prefwindow.vbox.pack_start(table, True, True, 0)
        self.prefwindow.show_all()
        self.prefwindow.connect("response", self.on_options_ok_clicked)
        response=self.prefwindow.run()
        self.prefwindow.destroy()
        return response

    def on_options_ok_clicked(self, widget, response_id):
        if not response_id == Gtk.ResponseType.ACCEPT:
            return response_id
        self.description = gtk_str(self.entryList[0].get_text())
        if self.description == "":
            logging.debug("A description is required - setting to default")
            self.description = "Uploaded from pytrainer"
        self.tags = gtk_str(self.entryList[1].get_text())
        self.visibility = gtk_str(self.entryList[2].get_active_text())
        self.makeanon = self.entryList[3].get_active()
        logging.debug("Description: %s, tags: %s, visibility: %s, makeanon: %s" % ( self.description, self.tags, self.visibility, self.makeanon) )

    def areaSelect(self,dc):
        """
            Open a window with OSM map so user could select his private/anonymized area - 
            all GPX dots in this area will be removed before uploading to OSM
        """       
        try:
            wTree = Gtk.glade.XML(self.pytrainer_main.data_path+"extensions/openstreetmap/OSM_AnonSelection.glade")
            self.privAreaWindow = wTree.get_widget("OSM_AnonSelection")
            dic = {
                "on_buttonOk_clicked" : self.privArea_Ok,
                "on_buttonCancel_clicked" : self.privArea_Cancel
            }
            wTree.signal_autoconnect( dic )
            mapviewer = MapViewer(self.pytrainer_main.data_path, pytrainer_main=self.pytrainer_main, box=wTree.get_widget("mapBox"))
            json=None
            if 'privPolygon' in self.options:
                json=self.options['privPolygon']
            htmlfile = Osm(data_path=self.pytrainer_main.data_path, waypoint=json, pytrainer_main=self.pytrainer_main).selectArea()
            mapviewer.display_map(htmlfile=htmlfile)
            mapviewer.moz.connect('title', self.parseTitle) 
            self.privAreaWindow.show()

        except Exception as e:
                msg = "Could not init map selection screen, Error: " + str(e)
                md = Gtk.MessageDialog(self.pytrainer_main.windowmain.window1, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, msg)
                md.set_title(_("Error"))
                md.run()
                md.destroy()
                return                

    def parseTitle(self, moz):
        "Event fired when document title was changed -> meaning polygon was changed"
        if moz.get_title() != "":
            self.mozTitle=str(moz.get_title())

    def privArea_Cancel(self,button):
        "Event fired when cancel button was pressed"
        self.privAreaWindow.destroy()
        
    def privArea_Ok(self,button):
        "Event fired when ok button was pressed"
        logging.debug(">> privArea_Ok")
        # save new private area polygon if changed
        if self.mozTitle=="":
            return
        # try parsing JSON
        try:
            # verify json is correct by deserializing and serializing it
            polygonString=dumps(loads(self.mozTitle))
            # try saving
            extensionDir = self.pytrainer_main.data_path + "/extensions" + "/openstreetmap"
            if not os.path.isdir(extensionDir):
                loggin.error(str(e))
                raise Exception("Could not find extension path: " + str(extensionDir))
            # save new options
            self.options['privPolygon'] = polygonString
            # convert dictionary to a lists set
            savedOptions = []
            for key in self.options:
                savedOptions.append((key,self.options[key]))
            # write new xml config file
            self.parent.setExtensionConfParams(extensionDir, savedOptions)
        except Exception as e:
            logging.error(str(e))    
            msg = _(str(e))
            md = Gtk.MessageDialog(self.pytrainer_main.windowmain.window1, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, msg)
            md.set_title(_("Error while saving extension configuration"))
            md.run()
            md.destroy()
        finally:
            self.privAreaWindow.destroy()
                                
    def multipart_encode(self, fields, files, boundary = None, buffer = None):
        '''
            Multipart encode data for posting
             from examples at from http://odin.himinbi.org/MultipartPostHandler.py & http://bitworking.org/projects/httplib2/doc/html/libhttplib2.html
        '''
        if boundary is None:
            boundary = mimetools.choose_boundary()
        if buffer is None:
            buffer = ''
        for (key, value) in fields:
            buffer += '--%s\r\n' % boundary
            buffer += 'Content-Disposition: form-data; name="%s"' % key
            buffer += '\r\n\r\n' + value + '\r\n'
        for (key, fd) in files:
            file_size = os.fstat(fd.fileno())[stat.ST_SIZE]
            filename = os.path.basename(fd.name)
            contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
            buffer += '--%s\r\n' % boundary
            buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename)
            buffer += 'Content-Type: %s\r\n' % contenttype
            # buffer += 'Content-Length: %s\r\n' % file_size
            fd.seek(0)
            buffer += '\r\n' + fd.read() + '\r\n'
        buffer += '--%s--\r\n\r\n' % boundary
        return boundary, buffer
        
    def make_gpx_private(self, gpx_file=None):
        '''
        wipes out private data from gpx files
        converts laps to waypoints
        '''
        logging.debug(">>")
        if gpx_file is None:
            return None
        
        filen = os.path.basename(gpx_file)
        tmpdir = self.pytrainer_main.profile.tmpdir
        anon_gpx_file = "%s/%s" % (tmpdir, filen)

        # get saved private area polygon
        pP=loads(self.options['privPolygon'])
        pP=pP['geometry']['coordinates'][0]
        # converts polygon's 2D matrix into a vector of just the lats or lons
        vector = lambda lonLat: [pP[i][lonLat] for i in range(len(pP))] # 0:lon, 1:lat
        # try reading private area's bounds, stored as [lon,lat]
        NE_LAT = max([pP[i][1] for i in range(len(pP))])
        NE_LON = max([pP[i][0] for i in range(len(pP))])
        SW_LAT = min([pP[i][1] for i in range(len(pP))])
        SW_LON = min([pP[i][0] for i in range(len(pP))])
        logging.info("Anonymizing Area: NE:%f,%f -> SW: %f,%f" % (NE_LON, NE_LAT, SW_LON, SW_LAT))
            
        # Config parameters, not used yet
        FILTER_BOX = True
        ERASE_TIME  = True
        LAP_TO_WAYPOINT = True

        tree = etree.parse(gpx_file)
        _xmlns = tree.getroot().nsmap[None]
        _trkpt_path = '{%s}trk/{%s}trkseg/{%s}trkpt' % (_xmlns, _xmlns, _xmlns)
        # namespace of gpx files
        NS = dict(ns='http://www.topografix.com/GPX/1/1')

        myroot =  tree.getroot()
        gpxdataNS = string.Template(\
            ".//{http://www.cluetrust.com/XML/GPXDATA/1/0}$tag")
        lapTag = gpxdataNS.substitute(tag="lap")
        endPointTag = gpxdataNS.substitute(tag="endPoint")
        triggerTag = gpxdataNS.substitute(tag="trigger")
        laps = tree.findall(lapTag)

        mygpx = tree.find('gpx')

        for lap in laps:
            trigger = lap.find(triggerTag)
            #  Watch out for manually triggered laps
            if trigger.text == 'manual':
                endPoint = lap.find(endPointTag)
                lat = endPoint.get("lat")
                lon = endPoint.get("lon")
                # Create waypt if not in home box
                try:
                    if not ((SW_LAT < float(lat) < NE_LAT) and (SW_LON < float(lon) < NE_LON)):
                        etree.SubElement(myroot, 'wpt', attrib= {'lat':lat, 'lon':lon})
                except:
                    pass
        etree.strip_attributes(myroot, 'creator')
                    
        # Wipe out home box
        for trkpt in tree.findall(_trkpt_path):
            lat = float(trkpt.attrib['lat'])
            lon = float(trkpt.attrib['lon'])
            if (lat < NE_LAT) & (lon < NE_LON) & (lat > SW_LAT) & (lon > SW_LON):
                par = trkpt.getparent()
                par.remove(trkpt)

        time = tree.xpath('//ns:trkpt/ns:time', namespaces=NS)
        for i in time:
            i.text = '1970-01-01T00:00:00+00:00'
            # osm regards <time> as mandatory. gnaa.

        ext = tree.xpath('//ns:gpx/ns:extensions', namespaces=NS)
        for i in ext:
            par = i.getparent()
            par.remove(i)
        meta = tree.xpath('//ns:gpx/ns:metadata', namespaces=NS)
        for i in meta:
            par = i.getparent()
            par.remove(i)
        ele = tree.xpath('//ns:trkpt/ns:ele', namespaces=NS)
        for i in ele:
            par = i.getparent()
            par.remove(i)

        # test schema on cleaned xml-tree
        # gpx.xsd from http://www.topografix.com/gpx.asp

        #xmlschema = etree.XMLSchema(etree.parse('gpx.xsd'))
        #xmlschema.validate(tree)

        # write new gpx file
        tree.write(anon_gpx_file, pretty_print=False, xml_declaration=True, encoding='UTF-8')
        logging.debug("<<")
        return anon_gpx_file