/
osmaware.py
394 lines (360 loc) · 17.8 KB
/
osmaware.py
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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# Author: francois schnell (http://francois.schnell.free.fr)
#
# Contributor: "awp.monkey" for a SAX patch (instead of ElementTree parsing)
# to allow a smaller memory footprint on big files
# (see issue1 on project's website)
#
# Released under the GPL license version 2
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
"""
Scans an OSM diff file and extract useful data to create a KML file to visualize
mapping activity of OSM.
It is not intended for mapping, just to give an awareness of the OSM activity.
"""
# Python imports
from xml.sax import make_parser
from xml.sax.handler import ContentHandler
from operator import itemgetter
import time
from datetime import datetime
# Local imports
import KML
class OSMaware(ContentHandler):
"""
Extracts mapping activity as a KML from an OSM diff file
(This is the contentHandler for a SAX parser)
"""
def __init__(self,debug=False,verbose=False,ele="10000",kml_version=0):
"""
Scans an .osc file and gather informations in lists and dictionaries
Args:
ele: lines elevation used in certain kml (0 to the ground)
Resulting instance properties:
self.osmData=[]
a list to contain them allow (see below)
self.osmNodes=[] (Only for kml v. 1 to minimize memory footprint)
a list containing nodes data (a dictionary per node with
the following keys: idNode, type, latitude,
longitude, timestamp, user)
self.statsUsers={}
a dict. containing data about the selected user.
single key = OSM user name
value= [a,b,c,d,[[x],[y],[z]]]
where:
a= total number of nodes from this user
b= number of created nodes
c= number of modified nodes
d= number of deleted nodes
[x]= a list of tuples values (lat,long) for the created nodes
[y]= a list of tuples values (lat,long) for the modified nodes
[z]= a list of tuples values (lat,long) for the deleted nodes
self.osmWays=[]
a list containing basic data about ways (a dict. per way with
the following keys: type, idWay, timestamp, user
"""
# elevation used in some kml
self.linesElevation=ele
# Data structure
self.osmData=[] # a list to contain them all
self.osmNodes=[] # for kml v1 only: a list containing all nodes (Python dictionaries)
self.osmWays=[] # a list to contain data about ways (Python dictionaries)
self.statsUsers={} # a dict. containing nodes and data relative to each user (key)
self.osmData.append(self.osmNodes)
self.osmData.append(self.osmWays)
self.osmData.append(self.statsUsers)
self.nodeCount = 0
self.wayCount = 0
self.relationCount = 0;
# feedback parameters
self.debug=debug
self.verbose=verbose
self.kml_version = kml_version
def startElement(self, name, attrs):
""" Analyse XML element each time it is given by the SAX Parser"""
# If elements of type create, modify or delete found will know the type
# of next "nodes" element encounter (return)
if name == "create" :
self.edit_type = "create"
return
if name == "modify" :
self.edit_type = "modify"
return
if name == "delete" :
self.edit_type = "delete"
return
# Analyse element of type "node"
if name == "node":
if self.debug:
print "Type=", self.edit_type,
print "ID node=",attrs.get("id"),
print "latitude=",attrs.get("lat"),
print "longitude=",attrs.get("lon"),
print "timestamp=",attrs.get("timestamp"),
print repr("user=",attrs.get("user"))
if self.kml_version == "1" :
self.osmNodes.append({
'idNode':attrs.get("id"),
'type':self.edit_type,
'latitude':attrs.get("lat"),
'longitude':attrs.get("lon"),
'timestamp':attrs.get("timestamp"),
'user':attrs.get("user"),
})
# Creating user's stats for nodes
user=attrs.get("user")
nodeType=self.edit_type
if self.statsUsers.has_key(user)==False:
#total,created,modified,deleted nodes and list of positions for this user
self.statsUsers[user]=[0,0,0,0,[[],[],[]]]
if self.statsUsers.has_key(user):
self.nodeCount += 1
self.statsUsers[user][0]+=1
if nodeType=="create":
self.statsUsers[user][1]+=1
self.statsUsers[user][4][0].append((attrs.get("lat"),attrs.get("lon")))
if nodeType=="modify":
self.statsUsers[user][2]+=1
self.statsUsers[user][4][1].append((attrs.get("lat"),attrs.get("lon")))
if nodeType=="delete":
self.statsUsers[user][3]+=1
self.statsUsers[user][4][2].append((attrs.get("lat"),attrs.get("lon")))
if name == "way" :
self.wayCount += 1
if (self.kml_version == "1") :
self.osmWays.append({
'type':self.edit_type,
'idWay':attrs.get("id"),
'timestamp':attrs.get("timestamp"),
'user':attrs.get("user"),
})
# Basic stats about the number of relations
if name == "relation" :
self.relationCount += 1
def globalStats(self,name="Stats Summary"):
"""
Returns an html stats summary (number of users, nodes, ways and a
table with one raw per user with link to their OSM homepage)
"""
print "Creating global stats..."
stats=u"Total number of users: "+str(len(self.statsUsers))+"<br>"
stats+=u"Total number of nodes created, deleted or modified: "+ str(self.nodeCount)+"<br>"
stats+=u"Total number of ways created, deleted or modified: "+ str(self.wayCount)+"<br><br>"
stats+="<table border='1' padding='3' width='600'>"
stats+="<tr><td>Author</td><td>Total (nodes)</td><td>Created</td><td>Modified</td><td>Deleted</td></tr>"
usersResult=sorted(self.statsUsers.items(), key=itemgetter(1),reverse=True)
for u in usersResult:
thisUser=unicode(u[0])
if thisUser !="None":
stats+=u'<tr><td><a href="http://www.openstreetmap.org/user/'+thisUser\
+'">'+thisUser+"</a></td>"
else:
stats+="<tr><td>None</td>"
stats+=u"<td>"+str(u[1][0])+"</td><td>"+str(u[1][1])\
+"</td><td>"+str(u[1][2])+"</td><td>"+str(u[1][3])+"</tr>"
return stats
def createKmlV0(self,kmlFileName="output"):
"""
The lightest kml version possible with only one "summarized" placemark
per user representing the last known position.
Args:
kmlFileName:
the name of the resulting kml (the osc filename per default)
Output:
Creates a kml file
"""
print "Creating KML file version 0 ..."
myKml=KML.KML(kmlFileName)
statsDescription=self.globalStats()
myKml.placemarkDescriptive(description=statsDescription,name=myKml.kmlTitle)
for userName, userStat in sorted(self.statsUsers.iteritems()):
myKml.folderHead("<![CDATA["+unicode(userName)\
+"("+str(self.statsUsers[userName][0])+")]]>")
thisLat=0
thisLong=0
for pathType in [0,1,2]:
if len(self.statsUsers[userName][4][pathType])!=0:
thisLat=self.statsUsers[userName][4][pathType][-1][0]
thisLong=self.statsUsers[userName][4][pathType][-1][1]
if pathType==0:
lineStyle="lineStyleCreated"
type="create"
if pathType==1:
lineStyle="lineStyleModified"
type="modify"
if pathType==2:
lineStyle="lineStyleDeleted"
type="delete"
userNodesStat=[self.statsUsers[userName][0], self.statsUsers[userName][1],
self.statsUsers[userName][2], self.statsUsers[userName][3]]
myKml.placemarkSummary(thisLat,thisLong,userName,type,userNodesStat)
myKml.folderTail()
myKml.close()
def createKmlV1(self,kmlFileName="output"):
"""
Creates a detailed KML output (one placemark per node)
placed in 3 folders ("created","modified","deleted")
Suitable only for reasonably small osc files (not days)
Args: -output name
"""
print "Creating KML file..."
myKml=KML.KML(kmlFileName)
statsDescription=self.globalStats()
myKml.placemarkDescriptive(description=statsDescription,name=myKml.kmlTitle)
for aType in ["create","modify","delete"]:
myKml.folderHead(aType)
for node in self.osmNodes:
if node["type"]==aType:
if self.verbose==True:
print node["latitude"], node["longitude"],node["idNode"],\
node["user"], node["timestamp"],node["type"]
if node["user"]==None: node["user"]="None"
myKml.placemark(node["latitude"],node["longitude"],node["idNode"],
node["user"],node["timestamp"],node["type"])
myKml.folderTail()
myKml.close()
def createKmlV2(self,kmlFileName="output",heightFactor=0,threshold=0.005):
"""
A version based on lines and polygons instead of placemarks
Args:
kmlFileName:
the name of the resulting kml (the osc filename per default)
threshold:
lat or long detla to link together the nodes (they aer not ways) to
better visualize that this nodes belong to the same user.
Output:
Creates a kml file
"""
print "Creating KML file..."
myKml=KML.KML(kmlFileName)
statsDescription=self.globalStats()
myKml.placemarkDescriptive(description=statsDescription,name=myKml.kmlTitle)
for userName, userStat in sorted(self.statsUsers.iteritems()):
myKml.folderHead("<![CDATA["+unicode(userName)\
+"("+str(self.statsUsers[userName][0])+")]]>")
for pathType in [0,1,2]:
## Extract created nodes-"path" for this user
# cut subpaths if next node is above the threshold
lonThreshold=threshold
latThreshold=threshold
#
paths=[] # list of cut paths
firstNode=True
thisPath=""
thisNode=""
for coordinate in self.statsUsers[userName][4][pathType]:
thisLat=coordinate[0]
thisLong=coordinate[1]
thisNode=thisLong+","+thisLat+","\
+str(heightFactor)+" "
if firstNode==True:
thisPath+=thisNode
prevLat=thisLat
prevLong=thisLong
firstNode=False
else:
#distanceThreshold=sqrt((thisLat-prevLat)**2 + (thisLong-prevLong)**2)
dLon=abs(float(thisLong)-float(prevLong))
dLat=abs(float(thisLat)-float(prevLat))
if (dLon > lonThreshold) and (dLat > latThreshold):
#print dLat,
paths.append(thisPath)
thisPath=""
elif (dLon < lonThreshold) and (dLat < latThreshold):
thisPath+=thisNode
prevLat=thisLat
prevLong=thisLong
paths.append(thisPath+thisNode)
#print paths
#if len(self.statsUsers[userName][4][0])!=0: pathCreated=pathCreated+thisNode
if pathType==0:
lineStyle="lineStyleCreated"
genre="Created"
if pathType==1:
lineStyle="lineStyleModified"
genre="Modified"
if pathType==2:
lineStyle="lineStyleDeleted"
genre="Deleted"
trackCut=1
for path in paths:
if path!="":
myKml.placemarkPath(pathName=genre+"P"+str(trackCut),
coordinates=path,style=lineStyle)
trackCut+=1
#if userName ==None: print "Anonymous users detected"
myKml.folderTail()
myKml.close()
if __name__=="__main__":
"""
Command-line version
Usage: python osmaware.py -i osmfile.osc [- o outputfile]
"""
import os, sys
from optparse import OptionParser
# Command line parameters
parser=OptionParser()
parser.add_option("-i", "--input",dest="osmInput",help="OSM input file (.osc)")
parser.add_option("-o", "--output",dest="kmlOutput",
help="KML output filename (without the .kml extension)")
parser.add_option("-k", "--kmlversion",dest="kmlVersion",
help="KML version desired (characterized by a number, see website)")
parser.add_option("-e", "--elevation",dest="linesElevation",
help="Elevation desired for certain kml genre (lines)")
(options,args)=parser.parse_args()
if options.osmInput==None:
print "I need an .osc file, type -h for help"
sys.exit(1)
# if no elevations is given lets assume 10 km per default for v2 kml
if options.linesElevation==None: options.linesElevation="100000"
# If an OSM HTML location is given in input fetch it first (wget must be in your path or current folder)
if (options.osmInput.find("http://")!=-1):
print "Found http input, attempting to retrieve the distant file..."
if sys.platform == 'darwin':
os.system('curl -O %s '%options.osmInput)
else:
os.system('wget %s '%options.osmInput)
options.osmInput=os.curdir+"/"+options.osmInput.split("/")[-1]
print options.osmInput
# If bz2 or gz archive is detected uncompress to osc file (using 7zip CLI on win)
if (options.osmInput.find(".bz2")>0) or (options.osmInput.find(".BZ2")>0)\
or (options.osmInput.find(".gz")>0) or (options.osmInput.find(".GZ")>0):
print "*",options.osmInput,"*"
archiveType=options.osmInput.split(".")[-1]
print "Trying to uncompress the archive of type ."+archiveType
if sys.platform == 'win32':
# 7za.exe must be installed in the app folder
if os.path.dirname(options.osmInput)=="":
os.system('7za.exe x -y "%s"' % (options.osmInput))
else:
os.system('7za.exe x -y "%s" -o"%s" ' % (options.osmInput,os.path.dirname(options.osmInput)))
if (sys.platform.find("darwin")!=-1) or (sys.platform.find("linux")!=-1):
if archiveType=="gz" or archiveType=="GZ":
os.system('gunzip "%s"' % options.osmInput)
elif archiveType=="bz2" or archiveType=="BZ2":
os.system('bzip2 -d "%s"' % options.osmInput)
options.osmInput=options.osmInput.rstrip("."+archiveType)
print "File uncompressed: ", options.osmInput
if options.kmlOutput==None: options.kmlOutput=os.path.basename(options.osmInput).rstrip(".osc")
if options.kmlVersion==None: options.kmlVersion="1"
myAwareness=OSMaware(debug=False,verbose=False,ele=options.linesElevation, kml_version=options.kmlVersion)
parser = make_parser()
parser.setContentHandler(myAwareness)
t0=datetime.now()
print "Starting parsing OSM file..."
parser.parse(options.osmInput)
#for userName, userStat in self.statsUsers.iteritems(): print userName, userStat
print "Number of contributors (users):", len(myAwareness.statsUsers)
print "Number of Nodes created, deleted or modified:", myAwareness.nodeCount
print "Number of Ways created, deleted or modified:", myAwareness.wayCount
print "Number of Realations created, deleted or modified:", myAwareness.relationCount
if options.kmlVersion=="2":
myAwareness.createKmlV2(options.kmlOutput,heightFactor=myAwareness.linesElevation)
if options.kmlVersion=="1":
myAwareness.createKmlV1(options.kmlOutput)
if options.kmlVersion=="0":
myAwareness.createKmlV0(options.kmlOutput)
print "Finished (took",(datetime.now()-t0).seconds,"seconds)"