line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package GPS::Point::Filter; |
2
|
1
|
|
|
1
|
|
27111
|
use strict; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
36
|
|
3
|
1
|
|
|
1
|
|
1502
|
use DateTime; |
|
1
|
|
|
|
|
193394
|
|
|
1
|
|
|
|
|
1023
|
|
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
our $VERSION='0.02'; |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
=head1 NAME |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
GPS::Point::Filter - Algorithm to filter extraneous GPS points |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
=head1 SYNOPSIS |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
use GPS::Point::Filter; |
14
|
|
|
|
|
|
|
my $gpf=GPS::Point::Filter->new; |
15
|
|
|
|
|
|
|
$gpf->addCallback(sample=>\&GPS::Point::Filter::callback_sample); |
16
|
|
|
|
|
|
|
my $status=$gpf->addPoint($point); |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
=head1 DESCRIPTION |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
GPS::Point::Filter uses a single pass linear algorithm to filter extraneous GPS points from a GPS feed. The filter uses three tests to determine whether to trigger a callback or not. |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
The most common use for this type of algorithm is to intelligently reduce the number of points before transmission over a limited bandwidth network. The filter properties will need to be tuned for particular networks and implementations. |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
=head1 USAGE |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
use GPS::Point::Filter; |
27
|
|
|
|
|
|
|
my $gpf=GPS::Point::Filter->new; |
28
|
|
|
|
|
|
|
$gpf->addCallback(sample=>\&GPS::Point::Filter::callback_sample); |
29
|
|
|
|
|
|
|
my $point=GPS::Point->new(time => time, |
30
|
|
|
|
|
|
|
lat => 39, |
31
|
|
|
|
|
|
|
lon => -77, |
32
|
|
|
|
|
|
|
speed => 25, |
33
|
|
|
|
|
|
|
heading => 135); |
34
|
|
|
|
|
|
|
my $status=$gpf->addPoint($point); |
35
|
|
|
|
|
|
|
printf "%s\n", $status if $status; |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
=head1 CONSTRUCTOR |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
=head2 new |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
my $gpf=GPS::Point::Filter->new( |
42
|
|
|
|
|
|
|
separation => 2000, #meters |
43
|
|
|
|
|
|
|
interlude => 1200, #seconds |
44
|
|
|
|
|
|
|
deviation => 500, #meters |
45
|
|
|
|
|
|
|
); |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=cut |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
sub new { |
50
|
1
|
|
|
1
|
1
|
11
|
my $this = shift(); |
51
|
1
|
|
33
|
|
|
7
|
my $class = ref($this) || $this; |
52
|
1
|
|
|
|
|
2
|
my $self = {}; |
53
|
1
|
|
|
|
|
3
|
bless $self, $class; |
54
|
1
|
|
|
|
|
6
|
$self->initialize(@_); |
55
|
1
|
|
|
|
|
2
|
return $self; |
56
|
|
|
|
|
|
|
} |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
=head2 initialize |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
=cut |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
sub initialize { |
63
|
1
|
|
|
1
|
1
|
2
|
my $self = shift(); |
64
|
1
|
|
|
|
|
5
|
%$self=@_; |
65
|
1
|
50
|
|
|
|
5
|
$self->separation(2000) unless defined $self->separation; |
66
|
1
|
50
|
|
|
|
4
|
$self->interlude(1200) unless defined $self->interlude; |
67
|
1
|
50
|
|
|
|
4
|
$self->deviation(500) unless defined $self->deviation; |
68
|
|
|
|
|
|
|
} |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
=head1 METHODS (Properties) |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
=head2 interlude |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
Sets or returns the filter interlude property. The interlude is defined as the period of time in seconds for which the previous filter point is still valid or not stale. The filter will trigger a callback if the GPS sample point does not move when the interlude is exceeded. |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
$gpf->interlude(1200); #default is 1200 seconds |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
=cut |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
sub interlude { |
81
|
2
|
|
|
2
|
1
|
2
|
my $self=shift; |
82
|
2
|
100
|
|
|
|
6
|
if (@_) { |
83
|
1
|
|
|
|
|
4
|
$self->{'interlude'}=shift; |
84
|
|
|
|
|
|
|
} |
85
|
2
|
|
|
|
|
5
|
return $self->{'interlude'}; |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
=head2 separation |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
Sets or returns the filter separation property. The separation is defined as the distance in meters between the previous filter point and the sample point. The filter will trigger a callback when then separation is exceeded. |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
$gpf->separation(2000); #default is 2000 meters |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
=cut |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
sub separation { |
97
|
2
|
|
|
2
|
1
|
3
|
my $self=shift; |
98
|
2
|
100
|
|
|
|
6
|
if (@_) { |
99
|
1
|
|
|
|
|
3
|
$self->{'separation'}=shift; |
100
|
|
|
|
|
|
|
} |
101
|
2
|
|
|
|
|
8
|
return $self->{'separation'}; |
102
|
|
|
|
|
|
|
} |
103
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
=head2 deviation |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
Sets or returns the filter deviation property. The deviation is defined as the distance in meters between the constant velocity predicted location of the previous filter point and the sample point. The filter will trigger a callback when then deviation is exceeded. |
107
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
$gpf->deviation(500); #default is 500 meters |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
=cut |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
sub deviation { |
113
|
2
|
|
|
2
|
1
|
3
|
my $self=shift; |
114
|
2
|
100
|
|
|
|
6
|
if (@_) { |
115
|
1
|
|
|
|
|
2
|
$self->{'deviation'}=shift; |
116
|
|
|
|
|
|
|
} |
117
|
2
|
|
|
|
|
5
|
return $self->{'deviation'}; |
118
|
|
|
|
|
|
|
} |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
=head1 METHODS |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
=head2 addCallback |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
Add a sub reference to the callback hash and returns the $gpf object. |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
$gpf->addCallback(label=>sub{print shift->latlon."\n"}); |
127
|
|
|
|
|
|
|
$gpf->addCallback(label=>\&mysub); |
128
|
|
|
|
|
|
|
sub mysub { |
129
|
|
|
|
|
|
|
my $point=shift;#GPS::Point (new point) |
130
|
|
|
|
|
|
|
my $gpf=shift; #GPS::Point::Filter with state info and previous point |
131
|
|
|
|
|
|
|
printf "Lat: %s, Lon: %s\n", $point->latlon; |
132
|
|
|
|
|
|
|
} |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
=cut |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
sub addCallback { |
137
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
138
|
0
|
0
|
|
|
|
|
if (scalar(@_) == 2) { |
139
|
0
|
|
|
|
|
|
my $key=shift; |
140
|
0
|
|
|
|
|
|
my $value=shift; |
141
|
0
|
0
|
|
|
|
|
$self->{'callback'}={} unless ref($self->{'callback'}) eq "HASH"; |
142
|
0
|
|
|
|
|
|
$self->{'callback'}->{$key}=$value; |
143
|
|
|
|
|
|
|
} else { |
144
|
0
|
|
|
|
|
|
die("Error: Method addCallback requires two arguments."); |
145
|
|
|
|
|
|
|
} |
146
|
0
|
|
|
|
|
|
return $self; |
147
|
|
|
|
|
|
|
} |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
=head2 deleteCallback |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
my $sub=$gpf->deleteCallback("label"); |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
=cut |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
sub deleteCallback { |
156
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
157
|
0
|
|
|
|
|
|
my $label=shift; |
158
|
0
|
|
|
|
|
|
return delete $self->{'callback'}->{$label}; |
159
|
|
|
|
|
|
|
} |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
=head2 addPoint |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
Adds a point to the filter to be tested and returns a short staus string. If the point is "extraneous", then the filter will not trigger a callback. |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
my $point=GPS::Point->new( |
166
|
|
|
|
|
|
|
lat => 39.000, #decimal degrees |
167
|
|
|
|
|
|
|
lon => -77.000, #decimal degrees |
168
|
|
|
|
|
|
|
speed => 50.0, #meters/second |
169
|
|
|
|
|
|
|
heading => 45.0, #degrees clockwise from North |
170
|
|
|
|
|
|
|
); |
171
|
|
|
|
|
|
|
my $status=$gpf->addPoint($point); |
172
|
|
|
|
|
|
|
if ($status) {print "added"} else {print "filtered"} |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=cut |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
sub addPoint { |
177
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
178
|
0
|
|
|
|
|
|
my $point=shift; |
179
|
0
|
0
|
|
|
|
|
die("Error: Point needs to be GPS::Point.") |
180
|
|
|
|
|
|
|
unless ref($point) eq "GPS::Point"; |
181
|
0
|
0
|
|
|
|
|
die("Error: Point needs to be at least GPS::Point 0.10.") |
182
|
|
|
|
|
|
|
unless $point->VERSION >= 0.10; |
183
|
0
|
0
|
|
|
|
|
unless (defined $self->point) { |
184
|
0
|
|
|
|
|
|
$self->execute($point); |
185
|
0
|
|
|
|
|
|
return $self->status(sprintf("start: %s", DateTime->now->datetime)); |
186
|
|
|
|
|
|
|
} else { |
187
|
0
|
|
|
|
|
|
my $interlude=$point->time - $self->point->time; |
188
|
0
|
0
|
|
|
|
|
if ($interlude > $self->interlude) { |
189
|
0
|
|
|
|
|
|
$self->execute($point); |
190
|
0
|
|
|
|
|
|
return $self->status(sprintf("interlude: %s", $interlude)); |
191
|
|
|
|
|
|
|
} else { |
192
|
0
|
|
|
|
|
|
my $separation=$point->distance($self->point); |
193
|
0
|
0
|
|
|
|
|
if ($separation > $self->separation) { |
194
|
0
|
|
|
|
|
|
$self->execute($point); |
195
|
0
|
|
|
|
|
|
return $self->status(sprintf("separation: %s", $separation)); |
196
|
|
|
|
|
|
|
} else { |
197
|
0
|
|
|
|
|
|
my $track=$self->point->track($interlude); |
198
|
0
|
|
|
|
|
|
my $deviation=$point->distance($track); |
199
|
0
|
0
|
|
|
|
|
print GPS::Point::Filter::callback_sample_string(Track=>$track) |
200
|
|
|
|
|
|
|
if $self->{'debug'}; |
201
|
0
|
0
|
|
|
|
|
if ($deviation > $self->deviation) { |
202
|
0
|
|
|
|
|
|
$self->execute($point); |
203
|
0
|
|
|
|
|
|
return $self->status(sprintf("deviation: %s", $deviation)); |
204
|
|
|
|
|
|
|
} else { |
205
|
0
|
|
|
|
|
|
return undef; |
206
|
|
|
|
|
|
|
} |
207
|
|
|
|
|
|
|
} |
208
|
|
|
|
|
|
|
} |
209
|
|
|
|
|
|
|
} |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
=head2 point |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
Sets or returns the GPS point stored in the GPS::Point::Filter object. |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
my $point=$gpf->point; |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
This point is set to the previous filter point when the callback is triggered. But, is updated just after the execute is complete. |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
=cut |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
sub point { |
223
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
224
|
0
|
0
|
|
|
|
|
if (@_) { |
225
|
0
|
|
|
|
|
|
$self->{'point'}=shift; |
226
|
|
|
|
|
|
|
} |
227
|
0
|
|
|
|
|
|
return $self->{'point'}; |
228
|
|
|
|
|
|
|
} |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
=head2 count |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
Sets or returns the count of number of points that have been filtered since the previous filter point; |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
$gpf->count; |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
=cut |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
sub count { |
239
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
240
|
0
|
0
|
|
|
|
|
if (@_) { |
241
|
0
|
|
|
|
|
|
$self->{'count'}=shift; |
242
|
|
|
|
|
|
|
} |
243
|
0
|
|
|
|
|
|
return $self->{'count'}; |
244
|
|
|
|
|
|
|
} |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
=head2 status |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
Sets or returns the status of the previous filter point. |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
=cut |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
sub status { |
253
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
254
|
0
|
0
|
|
|
|
|
if (@_) { |
255
|
0
|
|
|
|
|
|
$self->{'status'}=shift; |
256
|
|
|
|
|
|
|
} |
257
|
0
|
|
|
|
|
|
return $self->{'status'}; |
258
|
|
|
|
|
|
|
} |
259
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
=head1 METHODS (Internal) |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
=head2 callback |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
Returns the callback hash of sub references. |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
my $callback=$gpf->callback; #{label=>sub{}} |
268
|
|
|
|
|
|
|
my %callback=$gpf->callback; #(label=>sub{}) |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
Note: Callbacks are executed sorted by key. |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
=cut |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
sub callback { |
275
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
276
|
0
|
0
|
|
|
|
|
return wantarray ? %{$self->{'callback'}} : $self->{'callback'}; |
|
0
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
} |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
=head2 execute |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
Executes all sub references in the callback hash sorted by key. |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
The $point and the $gpf objects are passed to the sub routine as the first two arguments. |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
$gpf->execute; |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
=cut |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
sub execute { |
290
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
291
|
0
|
|
|
|
|
|
my $point=shift; |
292
|
0
|
|
|
|
|
|
my $callback=$self->callback; |
293
|
0
|
|
|
|
|
|
foreach my $key (sort keys %$callback) { |
294
|
0
|
|
|
|
|
|
&{$callback->{$key}}($point, $self); |
|
0
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
} |
296
|
0
|
|
|
|
|
|
$self->point($point); |
297
|
0
|
|
|
|
|
|
return $self; |
298
|
|
|
|
|
|
|
} |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
=head1 Functions (Convenience) |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=head2 callback_sample |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
A very simple callback example. |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
GPS::Point::Filter::callback_sample_string($point); |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
To register |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
$gpf->addCallback(sample=>\&GPS::Point::Filter::callback_sample); |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
=cut |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
sub callback_sample { |
315
|
0
|
|
|
0
|
1
|
|
my $point=shift; |
316
|
0
|
|
|
|
|
|
my $gpf=shift; |
317
|
0
|
|
|
|
|
|
print &callback_sample_string(Filter=>$point); |
318
|
|
|
|
|
|
|
} |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
=head2 callback_sample_string |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
Returns a formated string given a GPS::Point |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
my $string=GPS::Point::Filter::callback_sample_string($point); |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
=cut |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
sub callback_sample_string { |
329
|
0
|
|
|
0
|
1
|
|
my $label=shift; |
330
|
0
|
|
|
|
|
|
my $point=shift; |
331
|
0
|
|
|
|
|
|
return join("\t", $label, $point->time, |
332
|
|
|
|
|
|
|
$point->latlon, |
333
|
|
|
|
|
|
|
$point->speed, |
334
|
|
|
|
|
|
|
$point->heading). "\n"; |
335
|
|
|
|
|
|
|
} |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
=head1 TODO |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
I would like to implement a Kalman Filter in order to filter point data instead of the current interlude, separation, and deviation properties. |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
Add count of points filtered since previous point |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
Add status to gpf object |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
=head1 BUGS |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
Please report bugs to GEO-PERL list |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
=head1 SUPPORT |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
Please Try the GEO-PERL list |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
=head1 AUTHOR |
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
Michael R. Davis |
356
|
|
|
|
|
|
|
CPAN ID: MRDVT |
357
|
|
|
|
|
|
|
domain=>michaelrdavis,tld=>com,account=>perl |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
=head1 COPYRIGHT |
360
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
This program is free software licensed under the... |
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
The BSD License |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
The full text of the license can be found in the |
366
|
|
|
|
|
|
|
LICENSE file included with this module. |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
=head1 SEE ALSO |
369
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
L, L, L, L, L |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
=cut |
373
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
1; |