Package mosp :: Package external_persons :: Module external_data_manager
[hide private]
[frames] | no frames]

Source Code for Module mosp.external_persons.external_data_manager

  1  #!/bin/env python 
  2   
  3  """Service to collect and maintain data from external devices.""" 
  4   
  5  import os 
  6  import sys 
  7  sys.path.append("../..") 
  8  import time 
  9   
 10  import math 
 11  from math import sqrt, acos, degrees 
 12   
 13  from collections import namedtuple 
 14   
 15  from multiprocessing import Pipe 
 16  from multiprocessing import Process as MultiProcess 
 17   
 18  import hmac 
 19  import hashlib 
 20   
 21  from SimPy.SimulationRT import hold, passivate, Process 
 22   
 23  import cherrypy 
 24  from cherrypy import expose 
 25   
 26  from peach import fuzzy 
 27  from numpy import linspace 
 28   
 29  from mosp.geo import osm, utm 
 30  from external_person import ExternalPerson 
 31   
 32  #XXX DEBUG, remove this 
 33  from mosp.monitors import SocketPlayerMonitor 
 34   
 35  __author__ = "P. Tute, B. Henne" 
 36  __maintainer__ = "B. Henne" 
 37  __contact__ = "henne@dcsec.uni-hannover.de" 
 38  __copyright__ = "(c) 2012, DCSec, Leibniz Universitaet Hannover, Germany" 
 39  __license__ = "GPLv3" 
 40   
 41  HMAC_KEY_DEFAULT = 'omfgakeywtfdoidonow?' 
 42   
 43  MIN_ACCURACY = 100 # meter 
 44  LOCATION_CACHE_SIZE = 2 # should be 2 at last! 
45 46 -class ConnectionService(object):
47 - def __init__(self, address, port, conn, map_path, free_move_only, hmac_key):
48 self.sign = hmac.HMAC(hmac_key, digestmod=hashlib.sha256) 49 self.conn = conn 50 cherrypy.config.update({'server.socket_host': address, 51 'server.socket_port': port, 52 'tools.staticdir.on': True, 53 'tools.staticdir.dir': os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../mosp_tools/external_device_slippy_map_client/'), 54 #'log.screen': False, 55 }) 56 self.MatchingData = namedtuple('MatchingData', 'matched_way x y acc') 57 self.Point = namedtuple('Point', 'x y time') 58 self.last_match = {} # {id: MatchingData} 59 self.received_points = {} # {id: [Point, ...]} 60 self.matches = {} # {id: {time: MatchingData}} 61 self.need_init = [] 62 self.known_times = {} # {id: {time: [lat, lon, acc]}} 63 self.free_move_only = free_move_only 64 65 self.geo = osm.OSMModel(map_path) 66 self.geo.initialize(sim=None, enable_routing=False) 67 self.min_x = self.geo.bounds['min_x'] 68 self.max_x = self.geo.bounds['max_x'] 69 self.min_y = self.geo.bounds['min_y'] 70 self.max_y = self.geo.bounds['max_y'] 71 72 # init for fuzzy logic 73 # XXX value taken from paper, might need improvement 74 self.curve_center = 7 75 self.short_distance = fuzzy.DecreasingSigmoid(self.curve_center, 1) 76 self.long_distance = fuzzy.IncreasingSigmoid(self.curve_center, 1) 77 self.small_angle = fuzzy.DecreasingRamp(25, 65) 78 self.large_angle = fuzzy.IncreasingRamp(25, 65) 79 self.output_low = fuzzy.DecreasingRamp(3, 5) 80 self.output_avg = fuzzy.Triangle(3, 5, 7) 81 self.output_high = fuzzy.IncreasingRamp(5, 7) 82 self.c = fuzzy.Controller(linspace(0.0, 10.0, 100)) 83 # rule 1: IF distance IS short AND angle IS small THEN propability IS high 84 self.c.add_rule(((self.short_distance, self.small_angle), self.output_high)) 85 # rule 2: IF distance IS long AND angle IS large THEN propability IS low 86 self.c.add_rule(((self.long_distance, self.large_angle), self.output_low)) 87 # rule 3: IF distance IS short AND angle IS large THEN propability IS average 88 self.c.add_rule(((self.short_distance, self.large_angle), self.output_avg)) 89 # rule 4: IF distance IS long AND angle IS small THEN propability IS average 90 self.c.add_rule(((self.long_distance, self.small_angle), self.output_avg))
91 92 @expose
93 - def dummylocation(self, id='', lat='', lon='', acc='', speed='', bearing=''):
94 msg_sign = self.sign.copy() 95 msg_sign.update(id + lat + lon) 96 msg_hash = msg_sign.hexdigest() 97 self.location(id=id, lat=lat, lon=lon, acc=acc, hmac=msg_hash)
98 99 @expose
100 - def location(self, id='', lat='', lon='', acc='', hmac=''):
101 """Handle incoming location from $HOSTNAME:$PORT/location?$PARAMS.""" 102 time_received = time.time() 103 msg_sign = self.sign.copy() 104 msg_sign.update(id + lat + lon) 105 msg_hash = msg_sign.hexdigest() 106 107 # check HMAC 108 if msg_hash != hmac: 109 print 'HMAC hashes do not match!' 110 print 'hash of message', msg_hash 111 print 'hash received: ', hmac 112 return '<h1>Error!</h1>' 113 114 try: 115 # extract values from received strings 116 id_value = int(id) 117 lat_value = float(lat) 118 lon_value = float(lon) 119 x, y = utm.latlong_to_utm(lon_value, lat_value) 120 acc_value = float(acc) 121 if acc_value > MIN_ACCURACY: 122 print 'Received data with insufficient accuracy of {:f}. Minimal accuracy is {:d}'.format(acc_value, MIN_ACCURACY) 123 return '<h1>Not accurate enough!</h1>' 124 if (x - acc_value < self.min_x or 125 x + acc_value > self.max_x or 126 y - acc_value < self.min_y or 127 y + acc_value > self.max_y): 128 print 'Received data with out of bounds coordinates!' 129 print id + ' ' +lat + ' ' +lon + ' ' + acc 130 self.conn.send([id_value, None, None, x, y, time_received]) 131 #self.conn.send([id_value, None, None, x, y, time_received, lat_value, lon_value]) 132 return '<h1>Out of bounds!</h1>' 133 except ValueError: 134 # some value was not well formatted...ignore message 135 print 'Received invalid data!' 136 return '<h1>Values not well formatted!</h1>' 137 138 # send data to simulation 139 if self.free_move_only: 140 self.conn.send([id_value, None, None, x, y, time_received]) 141 else: 142 match = self.fuzzy_map_match(id_value, x, y, acc_value, time_received) 143 if match is not None: 144 self.conn.send(match) 145 else: 146 self.conn.send([id_value, None, None, x, y, time_received]) 147 #self.conn.send([id_value, None, None, x, y, time_received, lat_value, lon_value]) 148 149 # save received coordinates 150 if id not in self.received_points: 151 self.received_points[id_value] = [self.Point(x, y, time_received)] 152 else: 153 self.received_points[id_value].append(self.Point(x, y, time_received)) 154 while len(self.received_points[id_value]) > LOCATION_CACHE_SIZE: 155 del sorted(self.received_points[id_value], key=lambda p: p.time_received)[0] 156 157 print 'Received valid data: ID ' + id + ', lat ' +lat + ', lon ' +lon + ', acc ' + acc + ', at ' + str(time_received) 158 return '<h1>Correct!</h1>'
159
160 - def add_match(self, time, id, x, y, acc, way_segment):
161 """Add a new set of values to the known locations and remove an old one if necessary. 162 163 @param time: Timestamp of receive-time 164 @param id: id of the person 165 @param x: x coordinate of received location 166 @param y: y coordinate of received location 167 @param acc: accuracy of received location 168 @param way_segment: the current way segment the person was matched to 169 170 """ 171 values = self.MatchingData(way_segment, x, y, acc) 172 if id not in self.matches: 173 self.matches[id] = {} 174 self.matches[id][time] = values 175 self.last_match[id] = values 176 if len(self.matches[id]) > LOCATION_CACHE_SIZE: 177 del self.matches[id][sorted(self.matches[id].keys())[0]]
178
179 - def fuzzy_map_match(self, id, x, y, acc, time):
180 """Match the received coordinates to the OSM-map using fuzzy logic. 181 182 Algorithm is based on http://d-scholarship.pitt.edu/11787/4/Ren,_Ming_Dissertation.pdf (Chapter 4.3) 183 @param id: id of the person 184 @param x: x coordinate of received location 185 @param y: y coordinate of received location 186 @param acc: accuracy of received location 187 @param time: timestamp of receival 188 @return: a list with format [person_id, node_id_start, node_id_end, matched_x, matched_y, time_received] or None if no match was found 189 190 """ 191 if not id in self.matches or id in self.need_init: 192 print '\tinitial map', 193 if id in self.need_init: 194 print 'because of renewal', 195 self.need_init.remove(id) 196 print 197 if id not in self.received_points: 198 # at least two points are needed (current one and previous one) to be able to match 199 print 'not enough points yet' 200 return None 201 last_fix = sorted(self.received_points[id], key=lambda p: p.time, reverse=True)[0] 202 segment, matched_x, matched_y = self.initial_fuzzy_match(x, y, last_fix.x, last_fix.y, acc) 203 else: 204 print '\tsubsequent match' 205 match = self.subsequent_fuzzy_match(x, y, acc, self.last_match[id].matched_way, id) 206 if match is not None: 207 segment, matched_x, matched_y = match 208 else: 209 print 'Persons left matched segment, redo initial match.' 210 segment, matched_x, matched_y = self.initial_fuzzy_match(x, y, last_fix.x, last_fix.y, acc) 211 if segment is None: 212 print '\tno result segment' 213 # No segment found 214 return None 215 print '\tresult ', segment, matched_x, matched_y 216 self.add_match(time, id, matched_x, matched_y, acc, segment) 217 return [id, self.geo.map_nodeid_osmnodeid[segment.nodes[0].id], self.geo.map_nodeid_osmnodeid[segment.nodes[1].id], matched_x, matched_y, time]
218 #lon, lat = utm.utm_to_latlong(x, y, 32) 219 #return [id, self.geo.map_nodeid_osmnodeid[segment.nodes[0].id], self.geo.map_nodeid_osmnodeid[segment.nodes[1].id], matched_x, matched_y, time, lat, lon] 220
221 - def initial_fuzzy_match(self, x, y, previous_x, previous_y, acc, candidates=None):
222 """Perform initial map match based on fuzzy logic using the peach package. 223 224 @param x: x coordinate of received location 225 @param y: y coordinate of received location 226 @param previous_x: x coordinate of last received location 227 @param previous_y: y coordinate of last received location 228 @param acc: accuracy of received location 229 @param candidates: an iterable containing a set of predefined candidate segments (default is None) 230 @return: a tuple containing (identified segment, matched x, matched y) 231 232 """ 233 if candidates is None: 234 candidates = [obj for obj in self.geo.collide_circle(x, y, acc) if isinstance(obj, osm.WaySegment)] 235 236 # now calculate match possibilities for all nearby segments 237 results = {} 238 if candidates is None: 239 candidates = [obj for obj in self.geo.collide_circle(x, y, acc) if isinstance(obj, osm.WaySegment)] 240 for candidate in candidates: 241 closest_x, closest_y = candidate.closest_to_point(x, y) 242 distance = sqrt((x - closest_x)**2 + (y - closest_y)**2) 243 244 angle = self.calculate_angle((candidate.x_start, candidate.y_start), (candidate.x_end, candidate.y_end), 245 (previous_x, previous_y), (x, y)) 246 angle = angle if angle < 90 else abs(angle - 180) # ignore direction of road 247 # the order distance, angle must be consistant with the order in the rule definition! 248 results[candidate] = self.c(distance, angle) 249 250 # finally select the segment with highest match propability 251 if results: 252 match = max(results.items(), key=lambda item: item[1])[0] 253 match_x, match_y = match.closest_to_point(x, y) 254 # or None, if no match was found 255 else: 256 match = None 257 match_x, match_y = x, y 258 259 return (match, match_x, match_y)
260
261 - def subsequent_fuzzy_match(self, x, y, acc, segment, id):
262 """Perform subsequent matching along the identified segment and check for transition into new segment. 263 264 @param x: x coordinate of received location 265 @param y: y coordinate of received location 266 @param acc: accuracy of received location 267 @param segment: the way segment the person is currently moving on 268 @return: a tuple containing (identified segment, matched x, matched y) 269 270 """ 271 # Check if person is still colliding, detect movement away from road 272 if segment not in [obj for obj in self.geo.collide_circle(x, y, acc) if isinstance(obj, osm.WaySegment)]: 273 print 'Subsequent match detected movement away from matched street segment, performing initial match again!' 274 self.need_init.append(id) 275 return None, None, None 276 start_point = segment.nodes[0] 277 end_point = segment.nodes[1] 278 279 distance_threshold = acc #XXX arbitrary value! find real one! (maybe half of maximum move without update on android) 280 281 distance_to_start = sqrt((x - start_point.x)**2 + (y - start_point.y)**2) 282 distance_to_end = sqrt((x - end_point.x)**2 + (y - end_point.y)**2) 283 angle_to_start = self.calculate_angle((start_point.x, start_point.y), (end_point.x, end_point.y), 284 (start_point.x, start_point.y), (x, y)) 285 angle_to_end = self.calculate_angle((start_point.x, start_point.y), (end_point.x, end_point.y), 286 (x, y), (end_point.x, end_point.y)) 287 matched_x, matched_y = segment.closest_to_point(x, y) 288 if angle_to_start > 90 or angle_to_end > 90 or min(distance_to_start, distance_to_end) < distance_threshold: 289 # person left segment, reinitiate matching with next coordinates 290 #TODO maybe use segments of exit-node as new candidates 291 # contra: matching errors are carried 292 self.need_init.append(id) 293 return (segment, matched_x, matched_y)
294
295 - def calculate_angle(self, start1, end1, start2, end2):
296 """Calculate the angle between two lines identified by start and end points. 297 298 @param start1: starting point of line one 299 @type start1: tuple (x, y) 300 @param end1: ending point of line one 301 @type end1: tuple (x, y) 302 @param start2: starting point of line two 303 @type start2: tuple (x, y) 304 @param end2: ending point of line two 305 @type end2: tuple (x, y) 306 @return: angle in degrees as integer 307 308 """ 309 vector1 = [end1[0] - start1[0], end1[1] - start1[1]] 310 length1 = sqrt(sum((a*b) for a, b in zip(vector1, vector1))) 311 312 vector2 = [end2[0] - start2[0], end2[1] - start2[1]] 313 length2 = sqrt(sum((a*b) for a, b in zip(vector2, vector2))) 314 315 dotproduct = float(sum((a*b) for a, b in zip(vector1, vector2))) 316 angle = degrees(acos(dotproduct / (length1 * length2))) 317 angle = angle - 180 if angle > 180 else angle 318 return angle
319
320 321 -class ExternalDataManager(Process):
322 - def __init__(self, sim, address, port, map_path, free_move_only, hmac_key=HMAC_KEY_DEFAULT):
323 Process.__init__(self, name='ExternalDataManager', sim=sim) 324 self.sim = sim 325 self.conn, child_conn = Pipe() 326 self.service = ConnectionService(address, port, child_conn, map_path, free_move_only, hmac_key) 327 self.service_process = MultiProcess(target=cherrypy.quickstart, args=(self.service, )) 328 #self.service_process.daemon = True 329 self.service_process.start() 330 self.running = True 331 self.free_move_only = free_move_only
332
333 - def run(self):
334 for pers in self.sim.persons: 335 if isinstance(pers, ExternalPerson): 336 pers.current_coords = pers.current_coords_free_move 337 pers.calculate_duration = pers.calculate_duration_free_move 338 if self.free_move_only: 339 self.sim.geo.free_obj.add(pers) 340 while self.running: 341 sim = self.sim 342 geo = self.sim.geo 343 while(self.conn.poll()): 344 person_id, node_id_start, node_id_end, x, y, time_received = self.conn.recv() 345 #person_id, node_id_start, node_id_end, x, y, time_received, lat, lon = self.conn.recv() 346 person = sim.get_person(person_id) 347 if person == None: 348 print 'ExternalDataManager received unknown person id ', person_id, '. Discarded' 349 continue 350 if not isinstance(person, ExternalPerson): 351 print 'Received ID ', person_id, ' does not belong to external person. Discarded' 352 continue 353 person.last_received_coords = [x, y] 354 if node_id_start is not None: 355 if person in self.sim.geo.free_obj: 356 print 'Removing person with ID ', person_id, ' from free objects set!' 357 self.sim.geo.free_obj.remove(person) 358 person.new_next_node = geo.way_nodes_by_id[geo.map_osmnodeid_nodeid[node_id_end]] 359 person.new_last_node = geo.way_nodes_by_id[geo.map_osmnodeid_nodeid[node_id_start]] 360 person.need_next_target = True 361 else: 362 print 'Free move or no match found; free moving!' 363 self.sim.geo.free_obj.add(person) 364 #for m in sim.monitors: 365 # if isinstance(m, SocketPlayerMonitor): 366 # m.add_heatmap_blip(lat, lon, 3, (0.0, 0.0, 1.0, 0.4)) 367 #lon, lat = utm.utm_to_latlong(x, y, sim.geo.zone) 368 #for m in sim.monitors: 369 # if isinstance(m, SocketPlayerMonitor): 370 # m.add_heatmap_blip(lat, lon, 3, (1.0, 0.0, 0.0, 0.4)) 371 self.interrupt(person) 372 yield hold, self, 1
373
374 - def shutdown(self):
375 self.service_process.terminate()
376 377 378 if __name__ == '__main__': 379 #import guppy 380 #map = osm.OSMModel('../../data/hannover4.osm') 381 #map.initialize(sim=None, enable_routing=False) 382 #print 'Without routing\n\t', 383 #print guppy.hpy().heap() 384 #del map 385 #print 'Starting routing calc' 386 #map = osm.OSMModel('../../data/hannover4.osm') 387 #map.initialize(sim=None, enable_routing=True) 388 #print 'With routing\n\t', 389 #print guppy.hpy().heap() 390 391 #manager = ExternalDataManager('192.168.1.33', 8080) 392 service = ConnectionService('192.168.1.33', 8080, None, '../../data/hannover2.osm', True, HMAC_KEY_DEFAULT) 393 cherrypy.quickstart(service) 394