Browse Source

New ctrl + key support, new help screen and bug solved in route 'path finding'

New function Vtracert::ctrlUnicode(c) that returns the unicode representation of ctrl+letter.
New function Vtracert.help() that display an help screen
New path finding for routes, now they take the shorter horizontal direction (added a new propertie VtraceMap.scaledWidth updated by VtraceMap.genXlimits() to perfom this)
Various small code refactor and comments
Yann Weber 10 years ago
parent
commit
4711216378
3 changed files with 285 additions and 35 deletions
  1. 66
    9
      tracer.py
  2. 2
    0
      vtracemap.py
  3. 217
    26
      vtracert.py

+ 66
- 9
tracer.py View File

@@ -47,6 +47,7 @@ class Tracer:
47 47
 		
48 48
 		
49 49
 		self.prev = None
50
+		self.prevLonLat = None
50 51
 		
51 52
 		self.selected = [] #selected hop
52 53
 		
@@ -68,6 +69,7 @@ class Tracer:
68 69
 	def progressStatus(self):
69 70
 		self.vtr.drawStatus("Traceroute in progress : ...", self.vtr.font['status'],self.vtr.col['trace_progress'])
70 71
 		pass
72
+		
71 73
 	
72 74
 	## Generator that yield ips from a running traceroute
73 75
 	# @param The host to traceroute to
@@ -85,7 +87,6 @@ class Tracer:
85 87
 		#Update status bar
86 88
 		self.progressStatus()
87 89
 		self.vtr.flip()
88
-		yield self.src
89 90
 		
90 91
 		#Run traceroute process
91 92
 		p = Popen(['traceroute', dest_name], stdout=PIPE)
@@ -96,8 +97,17 @@ class Tracer:
96 97
 		ml = self.regetip.match(line)
97 98
 		if ml != None:
98 99
 			dest_ip = ml.group(1)
100
+			
101
+			(dlon,dlat) = self.ip2coord(dest_ip)
102
+			(x,y) = self.vtr.trmap.latlon2ortho(dlon, dlat)
103
+
104
+			pygame.gfxdraw.circle(self.vtr.surf['trace'],x,y,3,self.vtr.col['trace_end'])
105
+			
99 106
 		else:
100 107
 			dest_ip = None
108
+			
109
+		self.vtr.flip()
110
+		yield self.src
101 111
 		
102 112
 		#Reading traceroute output
103 113
 		while True:
@@ -155,11 +165,10 @@ class Tracer:
155 165
 		self.ips.append(ip)
156 166
 		
157 167
 		if ip != None:
158
-			#gir = self.gi.record_by_name(ip)
159
-			gir = self.gi.record_by_addr(ip)
160
-			if gir != None:
168
+			lonlat = self.ip2coord(ip)
169
+			if lonlat != None:
161 170
 				#print gir['latitude'], gir['longitude']
162
-				self.pts.append( ( gir['longitude'], gir['latitude']) )
171
+				self.pts.append(lonlat)
163 172
 			else:
164 173
 				nopts = True
165 174
 		else:
@@ -169,6 +178,14 @@ class Tracer:
169 178
 			self.pts.append(None)
170 179
 		pass
171 180
 	
181
+	## Get geoip info for ip
182
+	# @param ip to localisate
183
+	def ip2coord(self,ip):
184
+		gir = self.gi.record_by_addr(ip)
185
+		if gir != None:
186
+			return ( gir['longitude'], gir['latitude'])
187
+		return None
188
+	
172 189
 	## Populate data by running a traceroute
173 190
 	# @param dest_name The host
174 191
 	def trace(self,dest_name):
@@ -212,7 +229,7 @@ class Tracer:
212 229
 	# @param (lon,lat) Hop coordinates
213 230
 	# @param selected A boolean
214 231
 	# @return The coordinates of the hop on the screen as (x,y)
215
-	def drawNext(self, (lon, lat), selected = False):
232
+	def drawNext(self, (lon, lat), selected = False, last = False):
216 233
 		
217 234
 		if not selected :
218 235
 			#If not selected draw a circle
@@ -227,10 +244,50 @@ class Tracer:
227 244
 			cdrawfun(self.vtr.surf['trace'],x,y,3,self.vtr.col['trace_init'])
228 245
 		else:
229 246
 			#A new hop
230
-			cdrawfun(self.vtr.surf['trace'], x,y,5,self.vtr.col['trace_hop'])
247
+			
248
+			if last:
249
+				cdrawfun(self.vtr.surf['trace'], x,y,3,self.vtr.col['trace_hop'])
250
+			else:
251
+				cdrawfun(self.vtr.surf['trace'], x,y,5,self.vtr.col['trace_hop'])
252
+			
231 253
 			(px,py) = self.prev
232
-			pygame.gfxdraw.line(self.vtr.surf['trace'], px,py,x,y, self.vtr.col['trace_trace'])
254
+			
255
+			#Trying to find a symetric closer from the new hop
256
+			dw = self.vtr.trmap.scaledWidth
257
+			spx1 = px - dw # x prev symetric 1
258
+			spx2 = px + dw # x prev symetric 2
259
+			
260
+			(plon,_) = self.prevLonLat
261
+
262
+			splon1 = plon - 360 # prev lon symetric 1
263
+			splon2 = plon + 360 # prev lon symetric 2
264
+			
265
+			sd1 = abs(lon-splon1)
266
+			sd2 = abs(lon-splon2)
267
+			
268
+			
269
+			if sd1 < sd2:
270
+				spx = spx1
271
+				sx = x + dw
272
+				sd = sd1
273
+			else:
274
+				spx = spx2
275
+				sx = x - dw
276
+				sd = sd2
277
+			
278
+			d = abs(lon-plon)
279
+			
280
+			#drawLine = Tracer.drawArcLine
281
+			drawLine = pygame.gfxdraw.line
282
+			
283
+			if sd > d:
284
+				drawLine(self.vtr.surf['trace'], px,py,x,y, self.vtr.col['trace_trace'])
285
+			else:
286
+				#symetric is closer
287
+				drawLine(self.vtr.surf['trace'], spx,py,x,y, self.vtr.col['trace_trace'])
288
+				drawLine(self.vtr.surf['trace'], px,py,sx,y, self.vtr.col['trace_trace'])
233 289
 		
290
+		self.prevLonLat = (lon,lat)
234 291
 		self.prev = (x,y)
235 292
 		
236 293
 		return (x,y)
@@ -245,7 +302,7 @@ class Tracer:
245 302
 		for i,pt in enumerate(self.pts):
246 303
 			
247 304
 			if pt != None:
248
-				dcoord = self.drawNext(pt, (i in self.selected) )
305
+				dcoord = self.drawNext(pt, (i in self.selected), (i == len(self.pts)-1) )
249 306
 				self.coords.append(dcoord)
250 307
 				
251 308
 				if delay > 0:

+ 2
- 0
vtracemap.py View File

@@ -66,6 +66,7 @@ class TraceMap(object):
66 66
 		self.lonlat = TraceMap.lonlatFromFile(self.vtr.mapfile)
67 67
 		
68 68
 		self.xlimits = None
69
+		self.scaledWidth = 0 #Store the screen width if we will display all the map
69 70
 		self.lonlatminmax = (lonmin,lonmax,latmin,latmax)
70 71
 		self.xlimits = self.genXlimits(lonmin,lonmax,latmin,latmax)
71 72
 		
@@ -208,6 +209,7 @@ class TraceMap(object):
208 209
 			y1 = yl
209 210
 			y2 = ys
210 211
 		
212
+		self.scaledWidth = int(self.vtr.width * (float(self.vtr.width * 707) / (x2-x1)))
211 213
 		self.xlimits = (x2-x1,x1,y2-y1,y1)
212 214
 		return self.xlimits
213 215
 	

+ 217
- 26
vtracert.py View File

@@ -17,6 +17,33 @@
17 17
 #        You should have received a copy of the GNU General Public License
18 18
 #        along with pyMapTraceroute.  If not, see <http://www.gnu.org/licenses/>.
19 19
 #
20
+control_summary = [
21
+"+----------------------+----------------------------------+",
22
+"|                Controls summary                         |",
23
+"+----------------------+----------------------------------+",
24
+"+----------------------+----------------------------------+",
25
+"| ^h, ?                |  display this help               |",
26
+"+----------------------+----------------------------------+",
27
+"| esc, ^q              |  exit the programm or this help  |",
28
+"+----------------------+----------------------------------+",
29
+"+----------------------+----------------------------------+",
30
+"| [a-zA-Z0-9\.]        |  Host input                      |",
31
+"+----------------------+----------------------------------+",
32
+"| mid-mouse btn        |  paste clipboard into host input |",
33
+"+----------------------+----------------------------------+",
34
+"| enter                |  validate host input and         |",
35
+"|                      |  run traceroute                  |",
36
+"+----------------------+----------------------------------+",
37
+"| ^c                   |  clear host input                |",
38
+"+----------------------+----------------------------------+",
39
+"+----------------------+----------------------------------+",
40
+"| mouse scroll, +, -   : zoom in/out                      |",
41
+"+----------------------+----------------------------------+",
42
+"| arrow keys           : move on the map                  |",
43
+"+----------------------+----------------------------------+",
44
+"| left mouse btn press : select hops on the map           |",
45
+"+----------------------+----------------------------------+"
46
+]
20 47
 
21 48
 import websocket
22 49
 import random
@@ -41,7 +68,7 @@ class Vtracert:
41 68
 	# @param fonts Override default font type and size its a dict of tuple (fontType, fontSize), valid keys are : prompt, status
42 69
 	# @param promptText The prompt when reading the host to traceroute
43 70
 	# @param defaultInput Default host to traceroute
44
-	def __init__(self, mapdatfile, width = 1024, height = 768, cols = dict(), fonts = dict(), promptText = "Type the ip or hostname to traceroute :    %s_", defaultInput = "gnu.org"):
71
+	def __init__(self, mapdatfile, width = 1024, height = 768, cols = dict(), fonts = dict(), promptText = "Enter an ip or an hostname and press enter to traceroute :    %s_", defaultInput = "gnu.org"):
45 72
 		
46 73
 		pygame.init() #Pygame init
47 74
 		
@@ -51,6 +78,7 @@ class Vtracert:
51 78
 		self.height = height
52 79
 		#Colors
53 80
 		self.col = dict()
81
+		self.col['alpha'] = pygame.Color(0,0,0,0)
54 82
 		self.col['bg'] = pygame.Color(0,0,0)
55 83
 		self.col['map'] = pygame.Color(15,8,63)
56 84
 		self.col['prompt'] = pygame.Color(200,200,200)
@@ -58,11 +86,15 @@ class Vtracert:
58 86
 		self.col['cur_status'] = self.col['status']
59 87
 		self.col['cur_statusline'] = self.col['status']
60 88
 		self.col['trace_init'] = pygame.Color(0,0xf0,0)
89
+		self.col['trace_end'] = pygame.Color(0xee,0x33,0xee)
61 90
 		self.col['trace_hop'] = pygame.Color(0xf0,0xf0,0)
62 91
 		self.col['trace_trace'] = pygame.Color(0xf0,0,0)
63 92
 		self.col['trace_txt'] = pygame.Color(200, 200, 200, 125)
64 93
 		self.col['trace_txtsel'] = self.col['trace_hop']
65 94
 		self.col['trace_progress'] = pygame.Color(0,0xf0,0)
95
+		self.col['help_txt'] =  pygame.Color(200,200,200)
96
+		self.col['help_title'] = pygame.Color(120,30,230)
97
+		self.col['help_bg'] = pygame.Color(0,0,0)
66 98
 		for n, c in cols:
67 99
 			self.col[n] = pygame.Color(c)
68 100
 		#Fonts
@@ -71,13 +103,18 @@ class Vtracert:
71 103
 		self.font['status'] = pygame.font.Font(None,20)
72 104
 		self.font['cur_status'] = self.col['status']
73 105
 		self.font['trace_txt'] = pygame.font.Font(None,20)
106
+		
107
+		self.font['help_title'] = pygame.font.Font(None,48)
108
+		self.font['help_title'].set_underline(True);
109
+		self.font['help_title'].set_bold(True);
110
+		
111
+		self.font['help'] = pygame.font.SysFont('monospace', 16)
74 112
 		for n, (ft,fs) in fonts:
75 113
 			self.font[n] = pygame.font.Font(ft,fs)
76 114
 		#Prompt options
77 115
 		self.promptText = promptText
78 116
 		self.inputText = defaultInput
79
-		
80
-		
117
+		self.inputText = '1.202.15.14'
81 118
 		
82 119
 		#Event filtering
83 120
 		pygame.event.set_allowed(None)
@@ -91,19 +128,25 @@ class Vtracert:
91 128
 		self.surf['tracetxt'] = pygame.Surface((width, height),pygame.HWSURFACE | pygame.SRCALPHA)
92 129
 		self.surf['status'] = None
93 130
 		
131
+		for s in self.surf:
132
+			if self.surf[s] != None:
133
+				self.surf[s].fill(self.col['alpha'])
134
+		
135
+		self.help(0)
94 136
 		self.drawStatus('Loading...');
95 137
 		pygame.display.flip()
96 138
 		
97
-		
98 139
 		#Clipboard handling module initialisation
99 140
 		pygame.scrap.init()
100 141
 		
101 142
 		#Keyboard vars
102 143
 		self.caps = False
144
+		self.ctrl = False
103 145
 		
104 146
 		#Map info
105 147
 		self.trmap = vtracemap.TraceMap(self)
106 148
 		self.tracer = tracer.Tracer(self)
149
+		
107 150
 	
108 151
 	## Color accessor
109 152
 	# @param cname Color name
@@ -139,6 +182,96 @@ class Vtracert:
139 182
 			dopt[cname] = v
140 183
 		return dopt[cname]
141 184
 	
185
+	## Return the unicode representation of the combiation of ctrl+a letter key
186
+	@classmethod
187
+	def ctrlUnicode(classo, c):
188
+		return unicode(chr(ord(c.lower())-ord('a')+1))
189
+		pass
190
+	
191
+	##Display an help on the screen and wait for an KEYDOWN or QUIT event
192
+	def help(self, ttl = None):
193
+		scr = self.surf['screen']
194
+		helpTxts = [ 
195
+			"Vtraceroute help",
196
+			"",
197
+			"Vtraceroute is a traceroute program ( more info : man traceroute )",
198
+			"that displays hops on a map using geoipLookup.",
199
+			"",
200
+			"Controls summary :",
201
+			"",
202
+			"^h, ?                : display this help",
203
+			"esc, ^q              : exit the programm or this help",
204
+			"",
205
+			"[a-zA-Z0-9\.]        : Host input",
206
+			"mid-mouse btn        : paste clipboard into host input",
207
+			"enter                : validate host input and",
208
+			"                       run traceroute",
209
+			"^c                   : clear host input",
210
+			"",
211
+			"",
212
+			"mouse scroll, +, -   : zoom in/out",
213
+			"arrow keys           : move on the map",
214
+			"left mouse btn press : select hops on the map",
215
+			"","","",
216
+			"Press any key or mouse button to exit this help"
217
+		]
218
+		
219
+		tFont = self.font['help_title']
220
+		hFont = self.font['help']
221
+		hCol = self.col['help_txt']
222
+		tCol = self.col['help_title']
223
+		
224
+		ty = 50
225
+		lspacing = 3
226
+		hy = ty + tFont.get_linesize() + lspacing
227
+		hx = 40
228
+		
229
+		#Fill screen
230
+		scr.fill(self.col['help_bg'])
231
+		
232
+		#Print title
233
+		titleTxt = helpTxts.pop(0)
234
+		tsurf = tFont.render(titleTxt,1,tCol)
235
+		
236
+		tsc = tsurf.get_rect()
237
+		tsc.centerx = self.width/2 #center the title
238
+		tsc.y = ty
239
+		
240
+		scr.blit(tsurf,tsc)
241
+		
242
+		#Print help text
243
+		for line in helpTxts:
244
+			
245
+			if len(line) > 0:
246
+				tsurf = hFont.render(line,1,hCol)
247
+				scr.blit(tsurf,(hx, hy))
248
+			
249
+			hy += hFont.get_linesize()
250
+			hy += lspacing
251
+			#pygame.display.flip();time.sleep(0.05);
252
+		
253
+		pygame.display.flip()
254
+		
255
+		if ttl == None:
256
+			wait = True
257
+			while wait:
258
+				evts = pygame.event.get()
259
+				for evt in evts:
260
+					if evt.type in (pygame.QUIT, pygame.KEYDOWN, pygame.MOUSEBUTTONUP):
261
+						wait = False
262
+						break;
263
+		else:
264
+			time.sleep(ttl)
265
+		pass
266
+	
267
+	"""
268
+	## Return True 
269
+	# @c1 unicode1
270
+	# @c2 unicode2
271
+	def isCtrlUnicode(self, c, cu):
272
+		return (self.ctrl and Vtracert.ctrlUnicode(c) == cu)
273
+	"""
274
+	
142 275
 	## Blit a surface on the screen surface
143 276
 	# @param sname The surface name
144 277
 	# @param pos A tuple (x,y) for the blit position
@@ -146,15 +279,22 @@ class Vtracert:
146 279
 	def blitScreen(self, sname, pos = (0,0) ):
147 280
 		return self.surf['screen'].blit(self.surf[sname], pos)
148 281
 	
282
+	## Draw the map again and call Vtracert::flip()
283
+	# Call Vtracert::fillScreen()
284
+	# @see Vtracert::fillScreen(), Vtracert::flip()
149 285
 	def mapRedraw(self):
150 286
 		self.fillScreen()
151 287
 		self.trmap.draw()
152 288
 		self.flip()
153 289
 	
290
+	## Fill the screen with the 'bg' color
154 291
 	def fillScreen(self):
155 292
 		self.surf['screen'].fill(self.col['bg'])
156 293
 		pass
157 294
 	
295
+	## Blit all the surface is the right order but don't call pygame.display.flip()
296
+	# The status bar horizontal ruler is a draw and not a blit
297
+	# @see Vtracert::flip()
158 298
 	def blit(self):
159 299
 		self.blitScreen('map')
160 300
 		self.blitScreen('status')
@@ -164,6 +304,7 @@ class Vtracert:
164 304
 		pass
165 305
 	
166 306
 	## Blit all surface and flip display
307
+	# @see Vtracert::blit()
167 308
 	def flip(self, fill = True):
168 309
 		if fill:
169 310
 			self.fillScreen()
@@ -199,7 +340,7 @@ class Vtracert:
199 340
 		
200 341
 		self.surf['status'] = font.render(statusText, antialiasing, col)
201 342
 		
202
-		#self.blitScreen('status')
343
+		self.blitScreen('status')
203 344
 		
204 345
 		if line:
205 346
 			self.drawStatusLine()
@@ -218,20 +359,33 @@ class Vtracert:
218 359
 	def keyupEvt(self, evt):
219 360
 		if evt.key == 303 or evt.key == 304: #caps release
220 361
 			self.caps = False
221
-			print "Caps released"
362
+		elif evt.key == 305 or evt.key == 306: #ctrl release
363
+			self.ctrl = False
222 364
 		pass
223 365
 	
224 366
 	## Handles a KEYDOWN event
225 367
 	# @param evt Event
226 368
 	def keydownEvt(self, evt):
227 369
 		ccode = evt.key
228
-					
370
+		
371
+		c2u = Vtracert.ctrlUnicode
372
+		
229 373
 		allowedChar = range(ord('a'),ord('z')+1) 
230 374
 		allowedChar += range(ord('A'),ord('Z')+1)
231 375
 		allowedChar += range(ord('0'),ord('9')+1)
232 376
 		allowedChar += [ord('.'),ord('\b'),ord('\r'),ord('\n')]
233 377
 		
234 378
 		allowedChar = [ chr(cc) for cc in allowedChar ]
379
+		ctrlChar = [	c2u('q'), #^q
380
+						c2u('h'), #^h
381
+						c2u('n'), #^n
382
+						c2u('c'), #^n
383
+						'?',
384
+						u'\x1b' #escape button
385
+					]
386
+		
387
+		#print evt
388
+		#print self.ctrl
235 389
 		
236 390
 		if ccode in [273,274,275,276]: #Arrow keys
237 391
 			if ccode == 273:
@@ -244,17 +398,43 @@ class Vtracert:
244 398
 				d = 'left'
245 399
 			
246 400
 			self.trmap.zoom( (None, None), d)
401
+			self.tracer.drawTrace(0)
402
+			self.flip()
403
+			pygame.event.clear(pygame.MOUSEBUTTONUP)
247 404
 			
248 405
 		elif ccode == 303 or ccode == 304: #caps
249
-			caps = True
250
-		elif evt.unicode in allowedChar:
406
+			self.caps = True
407
+		elif evt.key == 305 or evt.key == 306: #ctrl release
408
+			self.ctrl = True
409
+		elif self.ctrl or cc in ctrlChar: # ctrl + key handle
251 410
 			
411
+			cc = evt.unicode
412
+			if cc == u'\x1b' or cc == c2u('q'): #quit (esc or ^q)
413
+				pygame.event.post(pygame.event.Event(pygame.QUIT))
414
+			elif cc == c2u('h') or cc == '?': #help ^h ? ^?
415
+				print "Help !!!!!!!!"
416
+				self.help()
417
+				self.flip()
418
+			elif cc == c2u('c'):
419
+				self.inputText = ''
420
+				self.drawPrompt()
421
+			elif cc == c2u('n'):
422
+				for cname in self.col:
423
+					c = self.col[cname]
424
+					c.r = 255 - c.r
425
+					c.g = 255 - c.g
426
+					c.b = 255 - c.b
427
+					self.col[cname] = c
428
+				self.mapRedraw()
429
+				
430
+		elif evt.unicode in allowedChar:
252 431
 			
253
-			cinput = evt.unicode
254
-			if cinput == '\b':
432
+			cc = evt.unicode
433
+				
434
+			if cc == '\b': #backspace
255 435
 				if len(self.inputText) > 0:
256 436
 					self.inputText = self.inputText[:-1]
257
-			elif cinput == '\r' or cinput == '\n':
437
+			elif cc == '\r' or cc == '\n':
258 438
 				print "OK, tracerouting %s ..."%self.inputText
259 439
 				dest = self.inputText
260 440
 				self.inputText = ""
@@ -266,7 +446,7 @@ class Vtracert:
266 446
 				self.flip()
267 447
 				
268 448
 			else:
269
-				self.inputText += cinput
449
+				self.inputText += cc
270 450
 			
271 451
 			self.drawPrompt()
272 452
 		pass
@@ -282,8 +462,9 @@ class Vtracert:
282 462
 				zoom = False
283 463
 				
284 464
 			self.trmap.zoom(evt.pos,zoom)
285
-			self.tracer.drawTrace()
465
+			self.tracer.drawTrace(0)
286 466
 			self.blit()
467
+			pygame.event.clear(pygame.MOUSEBUTTONUP)
287 468
 		#end scrollzoom
288 469
 		
289 470
 		elif evt.button == 1:
@@ -302,6 +483,7 @@ class Vtracert:
302 483
 			types = pygame.scrap.get_types()
303 484
 			for t in types:
304 485
 				print t,pygame.scrap.get(t)
486
+		
305 487
 		pass
306 488
 	
307 489
 	
@@ -312,23 +494,32 @@ class Vtracert:
312 494
 		self.mapRedraw();
313 495
 		self.flip()
314 496
 		
497
+		#Display small onetime help text
498
+		htext = self.font['status'].render("Type ^h or ? for help.",1, self.col['status'])
499
+		self.surf['screen'].blit(htext, (5, self.font['status'].get_linesize()+2))
500
+		pygame.display.flip()
501
+		
315 502
 		again = True
316 503
 		
317 504
 		while again:
505
+			"""
318 506
 			evts = pygame.event.get()
319 507
 			if len(evts) > 0:
320 508
 				
321 509
 				for evt in evts:
322
-					if evt.type == pygame.QUIT:
323
-						again = False
324
-						break
325
-					elif evt.type == pygame.KEYUP:
326
-						self.keyupEvt(evt)
327
-					elif evt.type == pygame.KEYDOWN:
328
-						self.keydownEvt(evt)
329
-					elif evt.type == pygame.MOUSEBUTTONUP: 
330
-						self.mouseboutonupEvt(evt)
331
-					
332
-					self.drawPrompt();
333
-					self.flip()
510
+			"""
511
+			evt = pygame.event.wait()
512
+			if evt != pygame.NOEVENT:
513
+				if evt.type == pygame.QUIT:
514
+					again = False
515
+					break
516
+				elif evt.type == pygame.KEYUP:
517
+					self.keyupEvt(evt)
518
+				elif evt.type == pygame.KEYDOWN:
519
+					self.keydownEvt(evt)
520
+				elif evt.type == pygame.MOUSEBUTTONUP: 
521
+					self.mouseboutonupEvt(evt)
522
+				
523
+				self.drawPrompt();
524
+				self.flip()
334 525
 	

Loading…
Cancel
Save