1
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
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
44 LOCATION_CACHE_SIZE = 2
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
55 })
56 self.MatchingData = namedtuple('MatchingData', 'matched_way x y acc')
57 self.Point = namedtuple('Point', 'x y time')
58 self.last_match = {}
59 self.received_points = {}
60 self.matches = {}
61 self.need_init = []
62 self.known_times = {}
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
73
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
84 self.c.add_rule(((self.short_distance, self.small_angle), self.output_high))
85
86 self.c.add_rule(((self.long_distance, self.large_angle), self.output_low))
87
88 self.c.add_rule(((self.short_distance, self.large_angle), self.output_avg))
89
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
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
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
132 return '<h1>Out of bounds!</h1>'
133 except ValueError:
134
135 print 'Received invalid data!'
136 return '<h1>Values not well formatted!</h1>'
137
138
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
148
149
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
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
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
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
219
220
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
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)
247
248 results[candidate] = self.c(distance, angle)
249
250
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
255 else:
256 match = None
257 match_x, match_y = x, y
258
259 return (match, match_x, match_y)
260
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
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
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
290
291
292 self.need_init.append(id)
293 return (segment, matched_x, matched_y)
294
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
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
329 self.service_process.start()
330 self.running = True
331 self.free_move_only = free_move_only
332
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
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
365
366
367
368
369
370
371 self.interrupt(person)
372 yield hold, self, 1
373
375 self.service_process.terminate()
376
377
378 if __name__ == '__main__':
379
380
381
382
383
384
385
386
387
388
389
390
391
392 service = ConnectionService('192.168.1.33', 8080, None, '../../data/hannover2.osm', True, HMAC_KEY_DEFAULT)
393 cherrypy.quickstart(service)
394