2 """ Handles data points and differences (deltas) between them. """
4 # Copyright (C) 2008 Laurens Van Houtven <lvh at laurensvh.be>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 thresholds
= consts
.DEFAULT_THRESHOLDS
25 """A container for GPS data.
27 Provides data scrubbing for GPS data and syntactic sugar for comparison:
28 p1 < p2 means that the deltas between p1 and p2 are significant (at least
29 one delta bigger than the threshold).
31 def __init__(self
, data
):
34 def update(self
, other
):
35 """Updates the data in this point with data from a new point."""
36 self
.data
.update(other
.data
)
38 def clear_message(self
):
39 """Clears the handler message."""
41 for key
in ['handlerid', 'handlermessage']:
45 def __getitem__(self
, key
):
46 """ Gets an entry from the point's dict. """
48 return self
.data
['key']
52 def __setitem__(self
, key
, value
):
53 """ Sets an entry in the point's dict. """
54 self
.data
[key
] = value
56 def __delitem__(self
, key
):
57 """ Deletes an entry from the point's dict. """
60 def __lt__(self
, other
):
61 """ Decides if a point is significantly different from another. """
62 for key
, threshold
in thresholds
.items():
63 diff
= delta(self
, other
, key
)
65 logging
.debug("Tried threshold %s, couldn't compare..." % key
)
67 elif diff
> threshold
:
68 logging
.debug("Passed threshold %s (diff=%2f, thresh=%2f)"
69 % (key
, diff
, threshold
))
71 else: # diff <= threshold
72 logging
.debug("Didn't pass threshold %s (diff=%2f, thresh=%2f)"
73 % (key
, diff
, threshold
))
75 return False # Thresholds exhausted, none triggered
77 def __gt__(self
, other
):
78 raise NotImplementedError
80 def delta(p1
, p2
, attribute
):
81 """Returns the difference in attribute between this point and another.
83 Differences are returned in SI units (seconds, meters...), except for
84 any angle (usually, this means true heading). Angles are returned in
85 degrees (because using radians would just be plain silly).
87 Equal points should always result in 0.0 (a float).
89 The distance code uses geopy's distance.distance function to calculate
90 the Vincenty distance, even though this is not really neccesary, since
91 sequential points generally aren't too far apart.
93 The heading code might look a bit strange at first. It accounts for the
94 fact that headings are cyclic: 359.0 and 1.0 are very close together and
95 should return a delta of 2.0, not |359.0 - 1.0| = 358.0.
97 This potentially raises issues when making U-turns, when the angle really
98 is ~180 degrees. This should be fixed by not making the heading threshold
99 too conservative, leading to several points that form the U-turn.
101 Returns None on failure.
105 get
= lambda attr
: get_attr_as_floats(p1
, p2
, attr
)
107 if attribute
== 'distance':
109 lat1
, lat2
= get('latitude')
110 lon1
, lon2
= get('longitude')
114 return geopy
.distance
.distance((lat1
, lon1
), (lat2
, lon2
)).km
* 1000
116 elif attribute
== 'heading':
118 h1
, h2
= get('heading')
119 delta_heading
= abs(h1
- h2
)
123 if delta_heading
< 180.0:
126 return 360.0 - delta_heading
128 else: # Generic case: dv = |v2 - v1|
130 v1
, v2
= get(attribute
)
134 delta_attr
= abs(v2
- v1
)
136 if attribute
!= "timestamp":
139 return int(delta_attr
)
141 def get_attr_as_floats(p1
, p2
, attribute
):
142 """Gets attributes from two data points as floats.
144 These attributes might be in string form depending on where they come
145 from (different sentences encode differently, unfortunately). This does
146 not incur any real penalty if the data already happens to be a float.
148 if attribute
not in p1
.data
or attribute
not in p2
.data
:
149 raise RuntimeError, 'Incomplete data'
151 v1
, v2
= p1
.data
[attribute
], p2
.data
[attribute
]
155 # XXX: I'm not entirely sure if anyone uses this... - lvh
156 def _default_thresholds():
157 """Gets the default thresholds from the consts module."""
159 return consts
.DEFAULT_THRESHOLDS
161 def update_tresholds(new_thresholds
= None):
162 """Updates the thresholds with the threshold data in the argument.
164 If no argument is supplied, it uses the default values.
166 Updating with anything else that is considered False by the interpreter
167 will reset the dicts to the default value. This includes the default value,
168 but also empty dictionaries. If an empty dict is passed, it will complain
169 loudly, because that might not be expected behaviour.
171 This will cause the dict to be updated, not replaced. Old thresholds will
172 not be overwritten. (If you want this instead, see set_thresholds.)
174 if not new_thresholds
:
175 logging
.error("Updating with empty dict, resetting thresholds...)
177 thresholds.update(new_thresholds or _default_thresholds())
179 def set_thresholds(new_thresholds = None):
180 """Sets the thresolds.
182 If no argument is supplied, it uses the default values.
184 Trying to set the thresholds to anything that evaluates to False also sets
185 them to the default values. This includes the default argument, but also an
188 If the thresholds were an empty dict, that would effectively mean no points
189 were ever considered important and no more points were sent. That's a very
190 bad (opaque) way of turning the tracker off.
192 thresholds = new_thresholds or _default_thresholds()