1
2
3
4 """A viewer for the MoSP simulator.
5
6 It uses a socket to receive geometric objects from the simulation and draws these onto a layer of OpenStreetMap-map-tiles.
7
8 @todo: draw triangles!!!
9 @todo: Zoom fit to BBox (Hmm...maybe trial and error is possible...might be tricky...)
10 @todo: Zoom to double click position (double clicks should be easy, accurate centering on coordinates is not supportet right now...could be hard)
11 """
12
13 import time
14 import os
15 import struct
16 import math
17
18 import socket
19
20 import pyglet
21
22 pyglet.options['debug_gl'] = False
23 from pyglet import gl
24 from pyglet.window import key, mouse
25
26 from lib import tilenames as tiles
27 from lib.tileloader2 import TileLoader
28 from lib.calculations import *
29
30 __author__ = "P. Tute"
31 __maintainer__ = "B. Henne"
32 __contact__ = "henne@dcsec.uni-hannover.de"
33 __copyright__ = "(c) 2011-2012, DCSec, Leibniz Universitaet Hannover, Germany"
34 __license__ = "GPLv3"
35
36
37 TILE_SIZE = 256
38 KEYBOARD_SCROLL_VALUE = 50
39
40
41 MESSAGE_TYPES = {'\x00': 'coords',
42 '\x01': 'point',
43 '\x02': 'rectangle',
44 '\x03': 'circle',
45 '\x04': 'triangle',
46 '\x05': 'text',
47 '\x06': 'heatmap',
48 '\x07': 'direct-text',
49 '\xFD': 'delete',
50 '\xFE': 'draw',
51 '\xFF': 'simulation_ended',
52 }
53
54
55 MESSAGE_SIZE = {'\x00': struct.calcsize('!dd'),
56 '\x01': struct.calcsize('!iddi4dd'),
57 '\x02': struct.calcsize('!i4di?4dd'),
58 '\x03': struct.calcsize('!iddi?4dd'),
59 '\x04': struct.calcsize('!i2d2d2d?4dd'),
60 '\x05': struct.calcsize('!iddiii4did'),
61 '\x06': struct.calcsize('!ddi4d'),
62 '\x07': struct.calcsize('!iiii4did'),
63 '\xFD': struct.calcsize('!i'),
64 }
65
66
67 ID_TYPE_PREFIX = {'\x00': 0,
68 '\x01': 0,
69 '\x02': 100000,
70 '\x03': 200000,
71 '\x04': 300000,
72 '\x05': 0,
73 }
74
75
76 PIXEL_DISTANCE = {}
77 for i in xrange(19):
78 left = tiles.xy2latlon(0, 0, i)[1]
79 right = tiles.xy2latlon(1, 0, i)[1]
80 PIXEL_DISTANCE[i] = (right - left) / TILE_SIZE
81
83
84 """A tool for displaying mosp-simulations.
85
86 @author: P. Tute
87
88 """
89
90 - def __init__(self, lat=0, lon=0, zoom=15, host='localhost', port=60001, ugly_drag=False, **kwargs):
91 """Initialize the viewer.
92
93 @param lat: Latitude to center view on (default 0).
94 @type lat: float
95 @param lon: Longitude to center view on (default 0).
96 @type lon: float
97 @param zoom: OSM-zomm-level to start with (default 16).
98 @type zoom: int in range [0, 18]
99 @param host: The host running the simulation (default 'localhost').
100 @type host: string
101 @param port: The port used by the simulation (default 60001).
102 @type port: int
103 @param ugly_drag: If set to True, dragging the map with the mouse will look uglier, but run faster (default False).
104 @param kwargs: @see http://pyglet.org/doc/api/pyglet.window.Window-class.html#__init__
105
106 """
107
108 super(SimViewer, self).__init__(**kwargs)
109
110
111 self.number_of_tiles = -1
112
113
114 self.drawing_offset = 0
115 self.zoom = zoom
116 self.center_x, self.center_y = tiles.latlon2xy(lat, lon, self.zoom)
117
118 self.center_lat, self.center_lon = tiles.xy2latlon(self.center_x, self.center_y, self.zoom)
119 self.default_lat, self.default_lon = self.center_lat, self.center_lon
120 self.offset_x , self.offset_y = 0, 0
121 self.ugly_drag = ugly_drag
122
123 self.cache_dir = '.cache'
124 self.data_dir = 'data'
125 self.screenshot_dir = 'screenshots'
126 self.not_found_image = os.path.join(self.data_dir, 'image_not_found.png')
127 try:
128 os.mkdir(self.cache_dir)
129 print 'No cache folder found. Creating it.'
130 except OSError:
131 print 'Found cache folder.'
132
133 try:
134 os.mkdir(self.screenshot_dir)
135 except OSError:
136 pass
137 timestamp = time.localtime()
138 self.current_sc_dir = str(timestamp.tm_year) + '.' + str(timestamp.tm_mon) + '.' + str(timestamp.tm_mday)
139 try:
140 os.mkdir(os.path.join(self.screenshot_dir, self.current_sc_dir))
141 except OSError:
142 pass
143
144
145 pyglet.gl.glClearColor(0.8,0.8,0.8,1.0)
146
147 self.mouse_drag = False
148 self.draw_fps = True
149
150 self.ended = False
151 self.draw_end_overlay = False
152 self.end_text1 = 'End of simulation.'
153 self.end_label1 = pyglet.text.Label(self.end_text1,
154 font_name='Times New Roman',
155 font_size=36,
156 color=(255, 255, 255, 255),
157 x=self.width/2, y=self.height/2,
158 anchor_x='center', anchor_y='center')
159 self.end_text2 = '(C)onnect to a new one? Show (l)ast screen? (Q)uit?'
160 self.end_label2 = pyglet.text.Label(self.end_text2,
161 font_name='Times New Roman',
162 font_size=18,
163 color=(255, 255, 255, 255),
164 x=self.width/2, y=self.height/2-80,
165 anchor_x='center', anchor_y='center')
166
167 self.copyright_text = u'Maps \xa9 OpenStreetMap contributors, CC-BY-SA'
168 self.copyright_label = pyglet.text.Label(self.copyright_text,
169 font_name='Times New Roman',
170 font_size=10,
171 color=(0, 0, 0, 255),
172 x=0, y=0,
173 anchor_x='right', anchor_y='bottom')
174 self.fps = 0
175 self.last_draw = 0
176 self.fps_label = pyglet.text.Label(str(int(self.fps)),
177 font_name='Times New Roman',
178 font_size=14,
179 color=(0, 0, 0, 255),
180 x=20, y=20,
181 anchor_x='center', anchor_y='center')
182
183 self.tiles = {}
184 self.tiles_used = {}
185 self.tileloader = TileLoader(tile_list=self.tiles, cache_dir=self.cache_dir)
186
187
188 self.drawing_batch = pyglet.graphics.Batch()
189 self.points = {}
190 self.rectangles = {}
191 self.circles = {}
192 self.triangles = {}
193 self.text_data = {}
194 self.text_objects = {}
195 self.direct_text_objects = {}
196 self.point_coords = {}
197 self.point_coords_offset = []
198 self.point_colors = {}
199 self.point_colors_all = []
200 self.point_vertex_list = self.drawing_batch.add(1, gl.GL_POINTS, None,
201 ('v2i/stream', (0, 0)),
202 ('c4d/stream', (0, 0, 0, 0)))
203 self.quad_coords = {}
204 self.quad_coords_offset = []
205 self.quad_colors = {}
206 self.quad_colors_all = []
207 self.quad_vertex_list = self.drawing_batch.add(1, gl.GL_QUADS, None,
208 ('v2i/stream', (0, 0)),
209 ('c4d/stream', (0, 0, 0, 0)))
210 self.triangle_coords = {}
211 self.triangle_coords_offset = []
212 self.triangle_colors = {}
213 self.triangle_colors_all = []
214 self.triangle_vertex_list = self.drawing_batch.add(1, gl.GL_TRIANGLES, None,
215 ('v2i/stream', (0, 0)),
216 ('c4d/stream', (0, 0, 0, 0)))
217 self.line_loop_coords = {}
218 self.line_loop_colors = {}
219 self.line_loop_vertex_lists = {}
220 self.polygon_coords = {}
221 self.polygon_colors = {}
222 self.polygon_vertex_lists = {}
223 self.heatmap_batch = pyglet.graphics.Batch()
224 self.heatmap_data = []
225 self.heatmap_point_coords = []
226 self.heatmap_point_coords_offset = []
227 self.heatmap_point_colors = []
228 self.heatmap_point_list = self.heatmap_batch.add(1, gl.GL_POINTS, None,
229 ('v2i/stream', (0, 0)),
230 ('c4d/stream', (0, 0, 0, 0)))
231 self.heatmap_quad_coords = []
232 self.heatmap_quad_coords_offset = []
233 self.heatmap_quad_colors = []
234 self.heatmap_quad_list = self.heatmap_batch.add(1, gl.GL_QUADS, None,
235 ('v2i/stream', (0, 0)),
236 ('c4d/stream', (0, 0, 0, 0)))
237
238 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
239 self.socket.setblocking(0)
240 self.host = host
241 self.port = port
242 try:
243 print 'connecting'
244 self.socket.connect((self.host, self.port))
245 print 'connected'
246 except socket.error as (errno, message):
247 if errno == 115:
248
249
250
251 pass
252 elif errno == 36:
253 pass
254 else:
255 print 'error while connecting'
256 print '\t', errno, message
257 raise
258
259 pyglet.clock.schedule_interval(self.receive, 1/30.0)
260 pyglet.clock.schedule_interval(self.on_draw, 1/60.0)
261 pyglet.clock.schedule_interval(self.reload_tiles, 0.5)
262
264 """Receive data from a given port and parse it to create drawable objects.
265
266 @param dt: Time since last call. Necessary for pyglet-scheduling, ignored here.
267 @author: P. Tute
268
269 """
270
271 new_points = {}
272 new_rects = {}
273 new_circles = {}
274 new_triangles = {}
275 new_texts = {}
276 new_heatmap = []
277 while True:
278 try:
279 message_type = self.socket.recv(1)
280 if message_type == '':
281
282 break
283 if MESSAGE_TYPES[message_type] == 'delete':
284 type = self.socket.recv(1)
285 id = struct.unpack('!i', self.socket.recv(MESSAGE_SIZE[message_type]))[0]
286 self.remove_drawing(0, type, id)
287 elif MESSAGE_TYPES[message_type] == 'coords':
288 lat, lon = struct.unpack('!dd', self.socket.recv(MESSAGE_SIZE[message_type]))
289 self.default_lat, self.default_lon = lat, lon
290 self.center_x, self.center_y = tiles.latlon2xy(lat, lon, self.zoom)
291 self.center_x -= 1
292 self.center_lat, self.center_lon = tiles.xy2latlon(self.center_x, self.center_y, self.zoom)
293 self.offset_x , self.offset_y = 0, 0
294 self.update_tiles()
295 elif MESSAGE_TYPES[message_type] == 'point':
296
297 data = self.socket.recv(MESSAGE_SIZE[message_type])
298 point = struct.unpack('!iddi4dd', data)
299 new_points[point[0] + ID_TYPE_PREFIX[message_type]] = point[1:]
300 if point[8] > 0:
301 pyglet.clock.schedule_once(self.remove_drawing, point[8], message_type, point[0])
302 elif MESSAGE_TYPES[message_type] == 'rectangle':
303
304 data = self.socket.recv(MESSAGE_SIZE[message_type])
305 rect = struct.unpack('!i4di?4dd', data)
306 new_rects[rect[0] + ID_TYPE_PREFIX[message_type]] = rect[1:]
307 if rect[11] > 0:
308 pyglet.clock.schedule_once(self.remove_drawing, rect[11], message_type, rect[0])
309 elif MESSAGE_TYPES[message_type] == 'circle':
310
311 data = self.socket.recv(MESSAGE_SIZE[message_type])
312 circle = struct.unpack('!iddi?4dd', data)
313 new_circles[circle[0] + ID_TYPE_PREFIX[message_type]] = circle[1:]
314 if circle[9] > 0:
315 pyglet.clock.schedule_once(self.remove_drawing, circle[9], message_type, circle[0])
316 elif MESSAGE_TYPES[message_type] == 'triangle':
317
318 data = self.socket.recv(MESSAGE_SIZE[message_type])
319 triangle = struct.unpack('!i2d2d2d?4dd', data)
320 new_triangles[triangle[0] + ID_TYPE_PREFIX[message_type]] = triangle[1:]
321 print triangle[12]
322 if triangle[12] > 0:
323 pyglet.clock.schedule_once(self.remove_drawing, triangle[12], message_type, triangle[0])
324 elif MESSAGE_TYPES[message_type] =='text':
325
326 data = self.socket.recv(MESSAGE_SIZE[message_type])
327 text_data = struct.unpack('!iddiii4did', data)
328 text_content = struct.unpack('!' + 'c' * text_data[10], self.socket.recv(struct.calcsize('!' + 'c' * text_data[10])))
329 text_data_list = list(text_data[1:])
330 text_data_list.append(text_content)
331 new_texts[text_data[0]] = text_data_list
332 if text_data[11] > 0:
333 pyglet.clock.schedule_once(self.remove_drawing, text_data[11], message_type, text_data[0])
334 elif MESSAGE_TYPES[message_type] == 'heatmap':
335
336 data = self.socket.recv(MESSAGE_SIZE[message_type])
337 hm = struct.unpack('!ddi4d', data)
338 self.heatmap_data.append(hm)
339 new_heatmap.append(hm)
340 elif MESSAGE_TYPES[message_type] == 'direct-text':
341
342 data = self.socket.recv(MESSAGE_SIZE[message_type])
343 id, x, y, fsize, r, g, b, a, tsize, ttl = struct.unpack('!iiii4did', data)
344 if x < 0:
345 x = self.width + x
346 if y < 0:
347 y = self.height + y
348 text_content = struct.unpack('!' + 'c' * tsize, self.socket.recv(struct.calcsize('!' + 'c' * tsize)))
349 self.direct_text_objects[id] = pyglet.text.Label("".join(text_content),
350 font_name='Times New Roman',
351 font_size=fsize,
352 color = tuple([int(i * 255) for i in (r, g, b, a)]),
353 x=x, y=y,
354 anchor_x='left', anchor_y='bottom')
355 elif MESSAGE_TYPES[message_type] == 'draw':
356 self.update_coordinates(new_points, new_rects, new_circles, new_triangles, new_texts, new_heatmap)
357 self.update_vertex_lists()
358 break
359 elif MESSAGE_TYPES[message_type] == 'simulation_ended':
360 self.ended = True
361 self.draw_end_overlay = True
362 else:
363
364 print '\twtf', repr(message_type)
365 break
366 except socket.error as (errno, msg):
367 if errno != 11:
368 raise
369 else:
370
371
372 break
373
374 - def update_coordinates(self, points=None, rects=None, circles=None, triangles=None, texts=None, hms=None, all=False):
375 """Calculate all coordinates necessary for drawing received points, rectangles and circles.
376
377 @param points: Points whose coordinates need to be calculated
378 @type points: dict
379 @param rects: Rectangles whose coordinates need to be calculated
380 @type rects: dict
381 @param circles: Circles whose coordinates need to be calculated
382 @type circles: dict
383 @param texts: Texts whose coordinates need to be calculated
384 @type texts: dict
385 @param hms: Heatmap-blips whose coordinates need to be calculated
386 @type hms: list
387 @param all: If all is True, all known coordinates will be redrawn. Other passed arguments will be ignored.
388 @type all: boolean
389 @author: P. Tute
390
391 """
392
393
394 if not points:
395
396 points = {}
397 if all:
398
399 points = self.points
400 for point in points:
401 self.points[point] = points[point]
402 (lat, lon, rad, r, g, b, a, ttl) = points[point]
403 x, y = latlon_to_xy(lat, lon, self.zoom, self)
404 if rad > 0:
405
406 coords = []
407 coords.append(x - rad)
408 coords.append(y - rad)
409 coords.append(x + rad)
410 coords.append(y - rad)
411 coords.append(x + rad)
412 coords.append(y + rad)
413 coords.append(x - rad)
414 coords.append(y + rad)
415 self.quad_coords[point] = coords
416 self.quad_colors[point] = [r, g, b, a] * 4
417 else:
418 self.point_coords[point] = [x, y]
419 self.point_colors[point] = [r, g, b, a]
420
421
422 if not rects:
423 rects = {}
424 if all:
425 rects = self.rectangles
426 for rect in rects:
427 self.rectangles[rect] = rects[rect]
428 (minlat, minlon, maxlat, maxlon, line_width, filled, r, g, b, a, ttl) = rects[rect]
429 x_left, y_bottom = latlon_to_xy(minlat, minlon, self.zoom, self)
430 x_right, y_top = latlon_to_xy(maxlat, maxlon, self.zoom, self)
431 coords = []
432 if filled:
433
434 coords.append(x_left)
435 coords.append(y_bottom)
436 coords.append(x_right)
437 coords.append(y_bottom)
438 coords.append(x_right)
439 coords.append(y_top)
440 coords.append(x_left)
441 coords.append(y_top)
442 self.quad_coords[rect] = coords
443 self.quad_colors[rect] = [r, g, b, a] * 4
444 else:
445
446
447
448
449
450 rad = 1 if line_width < 1 else line_width
451 self.line_loop_colors[rect] = []
452 for i in xrange(rad):
453 coords.extend((x_left + i,
454 y_bottom + i,
455 x_right - i,
456 y_bottom + i,
457 x_right - i,
458 y_top - i,
459 x_left + i,
460 y_top - i))
461 self.line_loop_colors[rect].extend((r, b, g, a) * 4)
462 self.line_loop_coords[rect] = coords
463 if not rect in self.line_loop_vertex_lists:
464 self.line_loop_vertex_lists[rect] = pyglet.graphics.vertex_list(len(coords) / 2,
465 'v2i', 'c4d')
466 if not circles:
467 circles = {}
468 if all:
469 circles = self.circles
470 for circle in circles:
471 self.circles[circle] = circles[circle]
472 (lat, lon, rad, filled, r, g, b, a, ttl) = circles[circle]
473 rad = self.meters_to_pixels(rad)
474 x, y = latlon_to_xy(lat, lon, self.zoom, self)
475 coords = bresenham_circle(x, y, rad)
476 colors = [r, g, b, a] * (len(coords) / 2)
477
478 if filled:
479 self.polygon_coords[circle] = coords
480 self.polygon_colors[circle] = colors
481 if not circle in self.polygon_vertex_lists:
482 self.polygon_vertex_lists[circle] = pyglet.graphics.vertex_list(len(coords) / 2,
483 'v2i', 'c4d')
484 else:
485 self.point_coords[circle] = coords
486 self.point_colors[circle] = colors
487
488 if not triangles:
489 triangles = {}
490 if all:
491 triangles = self.triangles
492 for tri in triangles:
493 self.triangles[tri] = triangles[tri]
494 (lat1, lon1, lat2, lon2, lat3, lon3, filled, r, g, b, a, ttl) = triangles[tri]
495 x_1, y_1 = latlon_to_xy(lat1, lon1, self.zoom, self)
496 x_2, y_2 = latlon_to_xy(lat2, lon2, self.zoom, self)
497 x_3, y_3 = latlon_to_xy(lat3, lon3, self.zoom, self)
498 coords = [x_1, y_1, x_2, y_2, x_3, y_3]
499 if filled:
500
501 self.triangle_coords[tri] = coords
502 self.triangle_colors[tri] = [r, g, b, a] * 3
503 else:
504
505 self.line_loop_colors[tri] = [r, g, b, a] * 3
506 self.line_loop_coords[tri] = coords
507 if not tri in self.line_loop_vertex_lists:
508 self.line_loop_vertex_lists[tri] = pyglet.graphics.vertex_list(len(coords) / 2,
509 'v2i', 'c4d')
510
511 if not texts:
512 texts = {}
513 if all:
514 texts = self.text_data
515 for text in texts:
516 self.text_data[text] = texts[text]
517 (lat, lon, x_off, y_off, fsize, r, g, b, a, tsize, ttl, content) = texts[text]
518 x, y = latlon_to_xy(lat, lon, self.zoom, self)
519 x += self.meters_to_pixels(x_off) + self.offset_x + self.drawing_offset
520 y += self.meters_to_pixels(y_off) + self.offset_y + self.drawing_offset
521 self.text_objects[text] = pyglet.text.Label("".join(content),
522 font_name='Times New Roman',
523 font_size=fsize,
524 color = tuple([int(i * 255) for i in (r, g, b, a)]),
525 x=x, y=y,
526 anchor_x='left', anchor_y='bottom')
527
528
529 if not hms:
530 hms = []
531 if all:
532 hms = self.heatmap_data
533 self.heatmap_point_coords = []
534 self.heatmap_point_colors = []
535 self.heatmap_quad_coords = []
536 self.heatmap_quad_colors = []
537 for hm in hms:
538 x, y = latlon_to_xy(hm[0], hm[1], self.zoom, self)
539 rad = hm[2]
540 color = hm[3:]
541 if rad > 0:
542 self.heatmap_quad_coords.append(x - rad)
543 self.heatmap_quad_coords.append(y - rad)
544 self.heatmap_quad_coords.append(x + rad)
545 self.heatmap_quad_coords.append(y - rad)
546 self.heatmap_quad_coords.append(x + rad)
547 self.heatmap_quad_coords.append(y + rad)
548 self.heatmap_quad_coords.append(x - rad)
549 self.heatmap_quad_coords.append(y + rad)
550 self.heatmap_quad_colors.extend(color * 4)
551 else:
552 self.heatmap_point_coords.append(x)
553 self.heatmap_point_coords.append(y)
554 self.heatmap_point_colors.extend(color)
555
557 """Add offsets to coordinates and update the used VertexLists."""
558
559
560 if len(self.point_coords) > 0:
561 coords = []
562 self.point_colors_all = []
563 for point in self.point_coords:
564 coords.extend(self.point_coords[point])
565 self.point_colors_all.extend(self.point_colors[point])
566 self.point_coords_offset = []
567 for i, coord in enumerate(coords):
568 if not i%2:
569
570 self.point_coords_offset.append(coord + self.offset_x + self.drawing_offset)
571 else:
572 self.point_coords_offset.append(coord + self.offset_y + self.drawing_offset)
573
574
575 if len(self.quad_coords) > 0:
576 coords = []
577 self.quad_colors_all = []
578 for quad in self.quad_coords:
579 coords.extend(self.quad_coords[quad])
580 self.quad_colors_all.extend(self.quad_colors[quad])
581 self.quad_coords_offset = []
582 for i, coord in enumerate(coords):
583 if not i%2:
584
585 self.quad_coords_offset.append(coord + self.offset_x + self.drawing_offset)
586 else:
587 self.quad_coords_offset.append(coord + self.offset_y + self.drawing_offset)
588
589
590 if len(self.triangle_coords) > 0:
591 coords = []
592 self.triangle_colors_all = []
593 for triangle in self.triangle_coords:
594 coords.extend(self.triangle_coords[triangle])
595 self.triangle_colors_all.extend(self.triangle_colors[triangle])
596 self.triangle_coords_offset = []
597 for i, coord in enumerate(coords):
598 if not i%2:
599
600 self.triangle_coords_offset.append(coord + self.offset_x + self.drawing_offset)
601 else:
602 self.triangle_coords_offset.append(coord + self.offset_y + self.drawing_offset)
603
604
605 if len(self.line_loop_coords) > 0:
606 for line in self.line_loop_coords:
607 coords = self.line_loop_coords[line]
608 colors = self.line_loop_colors[line]
609 coords_updated = []
610 for i, coord in enumerate(coords):
611 if not i%2:
612
613 coords_updated.append(coord + self.offset_x + self.drawing_offset)
614 else:
615 coords_updated.append(coord + self.offset_y + self.drawing_offset)
616 if self.line_loop_vertex_lists[line].get_size() != len(coords_updated) / 2:
617 self.line_loop_vertex_lists[line].resize(len(coords_updated) / 2)
618 self.line_loop_vertex_lists[line].vertices = coords_updated
619 self.line_loop_vertex_lists[line].colors = colors
620
621
622 if len(self.polygon_coords) > 0:
623 for tri in self.polygon_coords:
624 coords = self.polygon_coords[tri]
625 colors = self.polygon_colors[tri]
626 coords_updated = []
627 for i, coord in enumerate(coords):
628 if not i%2:
629
630 coords_updated.append(coord + self.offset_x + self.drawing_offset)
631 else:
632 coords_updated.append(coord + self.offset_y + self.drawing_offset)
633 if self.polygon_vertex_lists[tri].get_size() != len(coords_updated) / 2:
634 self.polygon_vertex_lists[tri].resize(len(coords_updated) / 2)
635 self.polygon_vertex_lists[tri].vertices = coords_updated
636 self.polygon_vertex_lists[tri].colors = colors
637
638
639 if len(self.heatmap_point_coords) > 0:
640 self.heatmap_point_coords_offset = []
641 for i, coord in enumerate(self.heatmap_point_coords):
642 if not i%2:
643
644 self.heatmap_point_coords_offset.append(coord + self.offset_x + self.drawing_offset)
645 else:
646 self.heatmap_point_coords_offset.append(coord + self.offset_y + self.drawing_offset)
647 if len(self.heatmap_quad_coords) > 0:
648 self.heatmap_quad_coords_offset = []
649 for i, coord in enumerate(self.heatmap_quad_coords):
650 if not i%2:
651
652 self.heatmap_quad_coords_offset.append(coord + self.offset_x + self.drawing_offset)
653 else:
654 self.heatmap_quad_coords_offset.append(coord + self.offset_y + self.drawing_offset)
655
660
661 - def get_image(self, x, y, z, layer='mapnik'):
662 """Load an image from the cache folder or download it.
663
664 Try to load from cache-folder first, download and cache if no image was found.
665 The image is placed in self.tiles by this method or by the TileLoader.load_images() after downloading.
666
667 @param x: OSM-tile number in x-direction
668 @type x: int
669 @param y: OSM-tile number in y-direction
670 @type y: int
671 @param z: OSM-zoom
672 @type z: int in range [0, 18]
673 @param layer: The used map layer (default 'mapnik')
674 @type layer: string (one of 'tah', 'oam' and 'mapnik')
675
676 """
677
678 url = tiles.tileURL(x, y, z, layer)
679 parts = url.split('/')[-4:]
680
681 if not os.path.exists(os.path.join(self.cache_dir, *parts)):
682
683 self.tileloader.enqueue_tile(x, y, z, layer)
684 return
685
686 try:
687 image = pyglet.image.load(os.path.join(self.cache_dir, *parts))
688 except:
689 image = pyglet.resource.image(self.not_found_image)
690 image.anchor_x = image.width / 2
691 image.anchor_y = image.height / 2
692 self.tiles[(x, y, z)] = image
693
695 """Recalculate drawing_offset and number_of_tiles if necessary, update tiles.
696
697 This is called by pyglet when the viewer is started or resized.
698
699 @see: http://pyglet.org/doc/api/pyglet.window.Window-class.html#on_resize
700
701 """
702
703 super(SimViewer, self).on_resize(width, height)
704 number_of_tiles = ((max(self.width, self.height) / TILE_SIZE) / 2) + 2
705 if number_of_tiles != self.number_of_tiles:
706 size_of_combined_map = (2 * number_of_tiles + 1) * TILE_SIZE
707 self.drawing_offset = (max(self.width, self.height) - size_of_combined_map) / 2
708 self.number_of_tiles = number_of_tiles
709 self.update_tiles()
710
712 """Update the self.tiles and self.tiles_used dicts after changes.
713
714 When necessary load new images and delete old ones.
715 This should not be called to often since it causes all coordinates of all drawings to be recalculated.
716
717 """
718
719 for y in xrange(-self.number_of_tiles, self.number_of_tiles + 1):
720 for x in xrange(-self.number_of_tiles, self.number_of_tiles + 1):
721
722 absolute_x = self.center_x + x
723 absolute_y = self.center_y - y
724 if (absolute_x, absolute_y, self.zoom) not in self.tiles:
725
726 self.get_image(absolute_x, absolute_y, self.zoom)
727
728
729 self.tiles_used[(x + self.number_of_tiles, y + self.number_of_tiles)] = (absolute_x, absolute_y, self.zoom)
730
731
732 for coord in self.tiles.keys():
733
734 if (not (self.center_x - 2 * self.number_of_tiles < coord[0] < self.center_x + 2 * self.number_of_tiles
735 and self.center_y - 2 * self.number_of_tiles < coord[1] < self.center_y + 2 * self.number_of_tiles)
736 and coord[2] == self.zoom):
737 del self.tiles[coord]
738
739 if not (self.zoom - 4 < coord[2] < self.zoom + 4):
740 del self.tiles[coord]
741
742 self.update_coordinates(all=True)
743 self.update_vertex_lists()
744 self.on_draw()
745
747 """Calculate the number of pixels that equal the given distance in.
748
749 @see: http://wiki.openstreetmap.org/wiki/Zoom_levels
750
751 @param m: Distance in meter
752 @type m: int
753 @returns: Distance in pixels
754 @rtype: int
755
756 """
757
758 earth_cirumference_meters = 637813.70
759 lat = math.radians(self.center_lat)
760 distance_per_pixel = earth_cirumference_meters*math.degrees(math.cos(lat))/2**(self.zoom+8)
761 numer_of_pixels = m / distance_per_pixel
762 return int(numer_of_pixels)
763
765 """Draw the screen.
766
767 This is periodically called by pyglet.
768
769 @param dt: Time since last call. Necessary for scheduling but ignored here.
770
771 """
772
773 self.tileloader.load_images()
774 self.clear()
775
776 for coord in self.tiles_used:
777 image = self.tiles[self.tiles_used[coord]]
778 x = coord[0]
779 y = coord[1]
780 image.blit(x * TILE_SIZE + self.offset_x + self.drawing_offset,
781 y * TILE_SIZE + self.offset_y + self.drawing_offset,
782 0)
783
784
785 gl.glEnable(gl.GL_BLEND)
786
787
788 if len(self.point_coords_offset) > 0:
789 if self.point_vertex_list.get_size() != len(self.point_coords_offset) / 2:
790 self.point_vertex_list.resize(len(self.point_coords_offset) / 2)
791 self.point_vertex_list.vertices = self.point_coords_offset
792 self.point_vertex_list.colors = self.point_colors_all
793 if len(self.quad_coords_offset) > 0:
794 if self.quad_vertex_list.get_size() != len(self.quad_coords_offset) / 2:
795 self.quad_vertex_list.resize(len(self.quad_coords_offset) / 2)
796 self.quad_vertex_list.vertices = self.quad_coords_offset
797 self.quad_vertex_list.colors = self.quad_colors_all
798 if len(self.triangle_coords_offset) > 0:
799 if self.triangle_vertex_list.get_size() != len(self.triangle_coords_offset) / 2:
800 self.triangle_vertex_list.resize(len(self.triangle_coords_offset) / 2)
801 self.triangle_vertex_list.vertices = self.triangle_coords_offset
802 self.triangle_vertex_list.colors = self.triangle_colors_all
803 self.drawing_batch.draw()
804 if len(self.heatmap_point_coords_offset) > 0:
805 if self.heatmap_point_list.get_size() != len(self.heatmap_point_coords_offset) / 2:
806 self.heatmap_point_list.resize(len(self.heatmap_point_coords_offset) / 2)
807 self.heatmap_point_list.vertices = self.heatmap_point_coords_offset
808 self.heatmap_point_list.colors = self.heatmap_point_colors
809 if len(self.heatmap_quad_coords_offset) > 0:
810 if self.heatmap_quad_list.get_size() != len(self.heatmap_quad_coords_offset) / 2:
811 self.heatmap_quad_list.resize(len(self.heatmap_quad_coords_offset) / 2)
812 self.heatmap_quad_list.vertices = self.heatmap_quad_coords_offset
813 self.heatmap_quad_list.colors = self.heatmap_quad_colors
814 self.heatmap_batch.draw()
815 for line in self.line_loop_vertex_lists.values():
816 line.draw(gl.GL_LINE_LOOP)
817 for poly in self.polygon_vertex_lists.values():
818 poly.draw(gl.GL_POLYGON)
819 for text in self.text_objects.values():
820 text.draw()
821 for text in self.direct_text_objects.values():
822 text.draw()
823
824
825 self.copyright_label.x = self.width
826 pyglet.graphics.draw(4, gl.GL_QUADS,
827 ('v2i', (self.copyright_label.x+2, self.copyright_label.y-2,
828 self.copyright_label.x-self.copyright_label.content_width-2, self.copyright_label.y-2,
829 self.copyright_label.x-self.copyright_label.content_width-2, self.copyright_label.y+self.copyright_label.content_height-1,
830 self.copyright_label.x+2, self.copyright_label.y+self.copyright_label.content_height-1)),
831 ('c4d', (1, 1, 1, 0.7) * 4))
832 self.copyright_label.draw()
833
834
835 if self.draw_fps:
836 now = time.time()
837 self.fps = self.fps*0.8 + 0.2 / (now - self.last_draw)
838 self.last_draw = now
839 self.fps_label.text = str(int(self.fps))
840 self.fps_label.draw()
841
842 if self.draw_end_overlay:
843 pyglet.graphics.draw(4, gl.GL_QUADS,
844 ('v2i', (0, 0, 0, self.height, self.width, self.height, self.width, 0)),
845 ('c4d', (0, 0, 0, 0.5) * 4))
846 self.end_label1.x = self.width / 2
847 self.end_label1.y = self.height / 2
848 self.end_label1.draw()
849 self.end_label2.x = self.width / 2
850 self.end_label2.y = self.height / 2 - 80
851 self.end_label2.draw()
852
854 """Zooms in or out of map by given factor.
855
856 @param factor: The factor to zoom by in OSM-zoom levels.
857 @type factor: int
858
859 """
860
861 if factor > 0 and self.zoom + factor <= 18 or factor < 0 and self.zoom - factor >= 0:
862 self.zoom += factor
863 self.center_x, self.center_y = tiles.latlon2xy(self.center_lat, self.center_lon, self.zoom)
864 self.center_lat, self.center_lon = tiles.xy2latlon(self.center_x, self.center_y, self.zoom)
865 self.update_tiles()
866
868 """This is called by pyglet whenever a key is pressed.
869
870 @see: http://pyglet.org/doc/api/pyglet.window.Window-class.html#on_key_press
871
872 """
873
874 super(SimViewer, self).on_key_press(symbol, modifiers)
875 if modifiers & key.MOD_CTRL:
876 if symbol == key.UP:
877 self.change_zoom(1)
878 elif symbol == key.DOWN:
879 self.change_zoom(-1)
880 elif symbol == key.F:
881 self.set_fullscreen(not self.fullscreen)
882 elif symbol == key.S:
883 self.take_screenshot()
884 elif symbol == key.UP:
885 self.offset_y -= KEYBOARD_SCROLL_VALUE
886 self.handle_offset()
887 elif symbol == key.DOWN:
888 self.offset_y += KEYBOARD_SCROLL_VALUE
889 self.handle_offset()
890 elif symbol == key.LEFT:
891 self.offset_x += KEYBOARD_SCROLL_VALUE
892 self.handle_offset()
893 elif symbol == key.RIGHT:
894 self.offset_x -= KEYBOARD_SCROLL_VALUE
895 self.handle_offset()
896 elif symbol == key.PLUS:
897 self.change_zoom(1)
898 elif symbol == key.MINUS:
899 self.change_zoom(-1)
900 elif symbol == key.F:
901 self.draw_fps = not self.draw_fps
902 elif symbol == key.Q:
903 self.close()
904 elif symbol == key.L:
905 if self.ended:
906 self.draw_end_overlay = not self.draw_end_overlay
907 elif symbol == key.C:
908 if self.ended:
909 self.ended = False
910 self.draw_end_overlay = False
911 self.reset_drawings()
912 self.reconnect()
913 elif symbol == key.SPACE:
914 self.center_x, self.center_y = tiles.latlon2xy(self.default_lat, self.default_lon, self.zoom)
915 self.center_x -= 1
916 self.center_lat, self.center_lon = tiles.xy2latlon(self.center_x, self.center_y, self.zoom)
917 self.offset_x , self.offset_y = 0, 0
918 self.update_tiles()
919
921 """Take a screenshot of the current simulation and save it with the current timestamp as it's name."""
922 shot = pyglet.image.get_buffer_manager().get_color_buffer()
923 time_of_shot = str(int(time.time()))
924 path = os.path.join(self.screenshot_dir, self.current_sc_dir)
925 tries = 0
926 filename = time_of_shot + '_' + str(tries) + '.png'
927 while os.path.exists(os.path.join(path, filename)):
928 tries += 1
929 filename = time_of_shot + '_' + str(tries) + '.png'
930 shot.save(os.path.join(path, filename))
931
933 """Called, when mouse-dragging is recognized.
934
935 @see: http://pyglet.org/doc/api/pyglet.window.Window-class.html#on_mouse_drag
936
937 """
938
939 if buttons & mouse.LEFT:
940 self.mouse_drag = True
941 self.offset_x = self.offset_x + dx
942 self.offset_y = self.offset_y + dy
943 if not self.ugly_drag:
944 self.update_vertex_lists()
945
947 """This is called by pyglet when a mouse button is released.
948
949 @see: http://pyglet.org/doc/api/pyglet.window.Window-class.html#on_mouse_release
950
951 """
952
953 if buttons & mouse.LEFT and self.mouse_drag:
954 self.mouse_drag = False
955 self.handle_offset(update_tiles=self.ugly_drag)
956
965
967 """Check, if new tiles need to be loaded because of the current offset.
968
969 If the offset is bigger than one tile, the appropriate tile
970 will become the new center and the offset is changed accordingly.
971
972 @param update_tiles: If True, self.update_tiles() will be called after adjusting for offset (default).
973
974 """
975
976
977
978 self.center_x -= int(self.offset_x / float(TILE_SIZE))
979 self.offset_x = (self.offset_x % TILE_SIZE if self.offset_x >= 0
980 else -(-self.offset_x % TILE_SIZE))
981 self.center_y += int(self.offset_y / float(TILE_SIZE))
982 self.offset_y = (self.offset_y % TILE_SIZE if self.offset_y >= 0
983 else -(-self.offset_y % TILE_SIZE))
984 self.center_lat, self.center_lon = tiles.xy2latlon(self.center_x, self.center_y, self.zoom)
985 if update_tiles:
986 self.update_tiles()
987
989 """Remove a drawing-object from the viewer.
990
991 The object is specified by it's type (in hexadecimal, see MESSAGE_TYPES) and it's unique id.
992
993 @param dt: Necessary for scheduling, ignored here
994 @type dt: int
995 @param type: Specifies which kind of object should be removed. Must be one of the types in MESSAGE_TYPES (in hexadecimal).
996 @type type: string
997 @param id: ID of the removed object
998 @type id: int
999
1000 """
1001
1002 id = id + ID_TYPE_PREFIX[type]
1003 type = MESSAGE_TYPES[type]
1004
1005 if type == 'point' and id in self.points:
1006 (lat, lon, rad, r, g, b, a, ttl) = self.points[id]
1007 del self.points[id]
1008 if rad > 0:
1009 del self.quad_coords[id]
1010 del self.quad_colors[id]
1011 else:
1012 del self.point_coords[id]
1013 del self.point_colors[id]
1014
1015
1016 self.point_vertex_list.delete()
1017 self.point_vertex_list = self.drawing_batch.add(1, gl.GL_POINTS, None,
1018 ('v2i', (0, 0)),
1019 ('c4d', (0, 0, 0, 0)))
1020 elif type == 'rectangle' and id in self.rectangles:
1021 (minlat, minlon, maxlat, maxlon, line_width, filled, r, g, b, a, ttl) = self.rectangles[id]
1022 del self.rectangles[id]
1023 if filled:
1024 del self.quad_coords[id]
1025 del self.quad_colors[id]
1026 else:
1027 del self.line_loop_coords[id]
1028 del self.line_loop_colors[id]
1029 del self.line_loop_vertex_lists[id]
1030 elif type == 'circle' and id in self.circles:
1031 (lat, lon, rad, filled, r, g, b, a, ttl) = self.circles[id]
1032 del self.circles[id]
1033 if filled:
1034 del self.polygon_coords[id]
1035 del self.polygon_colors[id]
1036 del self.polygon_vertex_lists[id]
1037 else:
1038 del self.point_coords[id]
1039 del self.point_colors[id]
1040 self.point_coords_offset = []
1041 self.point_colors_all = []
1042
1043
1044 self.point_vertex_list.delete()
1045 self.point_vertex_list = self.drawing_batch.add(1, gl.GL_POINTS, None,
1046 ('v2i', (0, 0)),
1047 ('c4d', (0, 0, 0, 0)))
1048 if type == 'triangle' and id in self.triangles:
1049 (lat1, lon1, lat2, lon2, lat3, lon3, filled, r, g, b, a, ttl) = self.triangles[id]
1050 del self.triangles[id]
1051 if filled:
1052 del self.triangle_coords[id]
1053 del self.triangle_colors[id]
1054 self.triangle_vertex_list.delete()
1055 self.triangle_vertex_list = self.drawing_batch.add(1, gl.GL_POINTS, None,
1056 ('v2i', (0, 0)),
1057 ('c4d', (0, 0, 0, 0)))
1058 else:
1059 del self.line_loop_coords[id]
1060 del self.line_loop_colors[id]
1061 del self.line_loop_vertex_lists[id]
1062 elif type == 'text' and id in self.text_objects:
1063 del self.text_data[id]
1064 del self.text_objects[id]
1065
1066 return
1067 elif type == 'direct-text' and id in self.direct_text_objects:
1068 del self.direct_text_objects[id]
1069 return
1070
1071 self.update_vertex_lists()
1072
1074 """Empty all vertex lists and set up new ones."""
1075 self.points = {}
1076 self.rectangles = {}
1077 self.circles = {}
1078
1079 self.point_coords = {}
1080 self.point_coords_offset = []
1081 self.point_colors = {}
1082 self.point_colors_all = []
1083 self.point_vertex_list.delete()
1084 self.point_vertex_list = self.drawing_batch.add(1, gl.GL_POINTS, None,
1085 ('v2i/stream', (0, 0)),
1086 ('c4d/stream', (0, 0, 0, 0)))
1087
1088 self.quad_coords = {}
1089 self.quad_coords_offset = []
1090 self.quad_colors = {}
1091 self.quad_colors_all = []
1092 self.quad_vertex_list.delete()
1093 self.quad_vertex_list = self.drawing_batch.add(1, gl.GL_QUADS, None,
1094 ('v2i/stream', (0, 0)),
1095 ('c4d/stream', (0, 0, 0, 0)))
1096 self.line_loop_coords = {}
1097 self.line_loop_colors = {}
1098 self.line_loop_vertex_lists = {}
1099 self.polygon_coords = {}
1100 self.polygon_colors = {}
1101 self.polygon_vertex_lists = {}
1102
1103 self.heatmap_data = []
1104 self.heatmap_point_coords = []
1105 self.heatmap_point_coords_offset = []
1106 self.heatmap_point_colors = []
1107 self.heatmap_point_list.delete()
1108 self.heatmap_point_list = self.heatmap_batch.add(1, gl.GL_POINTS, None,
1109 ('v2i/stream', (0, 0)),
1110 ('c4d/stream', (0, 0, 0, 0)))
1111 self.heatmap_quad_coords = []
1112 self.heatmap_quad_coords_offset = []
1113 self.heatmap_quad_colors = []
1114 self.heatmap_quad_list.delete()
1115 self.heatmap_quad_list = self.heatmap_batch.add(1, gl.GL_QUADS, None,
1116 ('v2i/stream', (0, 0)),
1117 ('c4d/stream', (0, 0, 0, 0)))
1118
1120 """Try to establish a new connection to the host and ports used when initialising.
1121
1122 This is mainly used when a new simulation was started and the viewer is supposed to be restarted.
1123
1124 """
1125
1126 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1127 self.socket.setblocking(0)
1128 try:
1129 print 'connecting'
1130 self.socket.connect((self.host, self.port))
1131 print 'connected'
1132 except socket.error as (errno, message):
1133 if errno == 115:
1134
1135
1136
1137 pass
1138 else:
1139 print 'error while connecting'
1140 print '\t', errno, message
1141 raise socket.error, (errno, message)
1142
1144 """This is called by pyglet when the viewer is closed.
1145
1146 The screenshot folder is deleted, if no screenshots were taken.
1147
1148 """
1149
1150 super(SimViewer, self).close()
1151 try:
1152 os.rmdir(os.path.join(self.screenshot_dir, self.current_sc_dir))
1153 except OSError:
1154
1155 pass
1156 try:
1157 os.rmdir(self.screenshot_dir)
1158 except OSError:
1159
1160 pass
1161
1162
1163 if __name__ == '__main__':
1164 viewer = SimViewer(52.382463, 9.717836, width=800, height=600, resizable=True, caption='MoSP-Simulation Viewer')
1165 pyglet.app.run()
1166