File Coverage

blib/lib/Geo/Track/Log.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Geo::Track::Log;
2              
3 3     3   88427 use 5.005001;
  3         12  
  3         103  
4 3     3   2436 use Time::Piece;
  3         52474  
  3         15  
5 3     3   1266 use XML::Simple;
  0            
  0            
6             use Carp;
7             use vars '*interpolate';
8             use strict;
9             use Data::Compare;
10             use Data::Dumper;
11             #use warnings;
12              
13             our $VERSION = '0.02';
14              
15             ###########################################################################
16             sub new {
17             my ($class, $params) = @_;
18             my $self = bless {}, $class;
19              
20             # set reasonable defaults
21              
22             # load the passed parameters
23             foreach my $key (%$params) {
24             $self->{$key} = $params->{$key};
25             }
26             return $self;
27             }
28              
29              
30             ###########################################################################
31             # loadTrackFromGPX - pass me a filehandle and I will load a track log.
32             #
33             sub loadTrackFromGPX {
34             my ($self, $file) = @_;
35             my $xml = XML::Simple->new(
36             ForceArray => [ 'trk', 'trkseg', 'trkpt' ],
37             KeyAttr => [],
38             NormaliseSpace => 2
39             );
40              
41             my $gpx = eval { $xml->XMLin($file) } or die "Invalid GPX track: $@";
42             for my $trk (@{$gpx->{trk}}) {
43             for my $seg (@{$trk->{trkseg}}) {
44             for my $pt (@{$seg->{trkpt}}) {
45             $pt->{time} =~ y/TZ/ /d; # "2004-08-29T01:44:11Z" -> "2004-08-29 01:44:11"
46             $self->addPoint({
47             lat => $pt->{lat},
48             long => $pt->{lon},
49             elevation => $pt->{ele},
50             timestamp => $pt->{time}
51             });
52             }
53             }
54             }
55             }
56              
57              
58             ###########################################################################
59             # loadTrackFromGarnix - pass me a filehandle and I will load a track log.
60             #
61             sub loadTrackFromGarnix {
62             my ($self, $FH) = @_;
63             while (my $st = <$FH>) {
64             chomp $st;
65             next unless $st =~ /^\s*-?\d/o;
66             $st =~ s/^\s+//gos;
67              
68             my $pt = $self->fixGarnixTrackLine($st);
69             $self->addPoint($pt);
70             }
71             }
72             sub output_track_text{
73             my $self = shift;
74             print "lat\t";
75             print "long\t";
76             my $pt = $self->{log}->[0];
77             foreach my $k (sort keys %$pt) {
78             next if ($k =~ /lat/i);
79             next if ($k =~ /long/i);
80             print $k . "\t";
81             }
82             print "\n";
83              
84             foreach my $pt (@{$self->{log}}) {
85             print $pt->{lat} . "\t";
86             print $pt->{long} . "\t";
87             foreach my $k (sort keys %$pt) {
88             next if ($k =~ /lat/i);
89             next if ($k =~ /long/i);
90             print $pt->{$k} . "\t";
91             }
92             print "\n";
93             }
94             }
95              
96              
97              
98              
99             # this has a problem!!! fixGarnixWayLine has the problem that
100             # waypoint names used to be space delimited, but waypoints from
101             # the rino have names that _can_ be in quotes and include spaces.
102             sub loadWayFromGarnix {
103             my ($self, $FH) = @_;
104             while (my $st = <$FH>) {
105             chomp $st;
106             next unless $st =~ /^\s*-?\d/o;
107             $st =~ s/^\s+//gos;
108              
109             my $pt = $self->fixGarnixWayLine($st);
110             $self->addPoint($pt);
111             }
112             }
113              
114             ###########################################################################
115             # fixGarnixTrackLine
116             # Take this:
117             #44? 3' 33.23" -123? 5' 0.07" 148.0 WGS84 00:50:19-2004/07/12 [1];
118             # And return a canonical $pt, a hashref to glory, or at least, the
119             # Garnix information in a handy form.
120             sub fixGarnixTrackLine {
121             my ($self, $st) = @_;
122            
123             my ($pt, @lat, @long, $date, $time);
124              
125             # this is a garnix line
126             #44? 3' 33.23" -123? 5' 0.07" 148.0 WGS84 00:50:19-2004/07/12 [1];
127             # the ? is 'really' a degree symbol
128              
129             # this splits that line based on spaces.
130             (@lat[0..2], @long[0..2], $pt->{elevation}, $pt->{datum},
131             $pt->{timestamp}, $pt->{segment}) = split /\s+/, $st;
132              
133             $pt->{lat} = dms_to_deg(@lat);
134             $pt->{long} = dms_to_deg(@long);
135             next unless $pt->{lat} and $pt->{long};
136              
137             ($pt->{time}, $pt->{date}) = split /-/, $pt->{timestamp};
138             $pt->{date} =~ s|/|-|g;
139             $pt->{timestamp} = $pt->{date} . ' ' . $pt->{time};
140             $pt->{segment} =~ s/\D//gos;
141              
142             # remove leading and trailing spaces from all fields
143             foreach my $f qw(lat long elevation timestamp date time segment) {
144             $pt->{$f} =~ s/(^\s+)|(\s+$)//g;
145             }
146              
147             return $pt
148             }
149              
150              
151              
152             sub fixGarnixWayLine {
153             my ($self, $st) = @_;
154             $st =~ s/^\s+//g;
155            
156             my ($pt, @lat, @long, $date, $time, $name, $comment);
157             my @rest;
158              
159             # this is a garnix line
160              
161             # way line
162             #38? 18' 11.5" -123? 3' 27.8" 0.0 WGS84 ADV2 "CRTD 14:37 15-OCT-00";
163              
164             # but this is also a wayline: note the space in the waypoint name
165             # 'FELIX CAFE'.
166             #33� 47' 14.77" -117� 51' 12.67" 55.0 WGS84 "FELIX CAFE" "" [knife N];
167            
168             # I think I am safe through the datum, then it becomes space delimited
169             # with optional quotes that mean disregard the space.
170              
171             # this splits that line based on spaces.
172             (@lat[0..2], @long[0..2], $pt->{elevation}, $pt->{datum},
173             @rest) = split /\s+/, $st;
174             my $rest = join ' ', @rest;
175              
176             # name and comment parsing
177              
178             # is the name in comments?
179             if ( $rest =~ s/^"([^"]+)"//) {
180             $pt->{name} = $1;
181             } else {
182             $rest =~ s/^([\S]+)\s//;
183             $pt->{name} = $1;
184             }
185              
186             # comment includes the waypoint symbol, but I can't deal
187             # with that at this point...
188             $pt->{comment} = $rest;
189              
190             $pt->{lat} = dms_to_deg(@lat);
191             $pt->{long} = dms_to_deg(@long);
192             return undef unless $pt->{lat} and $pt->{long};
193              
194             #($pt->{time}, $pt->{date}) = split /-/, $pt->{timestamp};
195             #$pt->{date} =~ s|/|-|g;
196             #$pt->{timestamp} = $pt->{date} . ' ' . $pt->{time};
197             #$pt->{segment} =~ s/\D//gos;
198              
199             # remove leading and trailing spaces from all fields
200             foreach my $f qw(lat long elevation ) {
201             $pt->{$f} =~ s/(^\s+)|(\s+$)//g;
202             }
203              
204             return $pt
205             }
206              
207             ###########################################################################
208             # addPoint - give me a hashref with at least lat, long, and timestamp
209             # and we will live sweetly on the good earth.
210             sub addPoint {
211             my ($self, $p) = @_;
212             $self->{dirty} = 1;
213             # this is to clear elevation non initialized warnings. but
214             # the positive assertion '0' is not actually correct. Damn
215             # but thinking can be hard.
216             $p->{elevation} = $p->{elevation} || 0;
217              
218             # take the hashref that was passed, and add it to this list of points.
219             push @{$self->{log}}, $p;
220             }
221              
222              
223             ###########################################################################
224             # take a garnix track log formatted string containing lat or long, return
225             # a decimal degree. Ror now assume positive (north) lat and negative (western) long.
226             sub dms_to_deg {
227             my ($deg, $min, $sec) = @_;
228             s/\D+$//o for ($deg, $min, $sec);
229             my $dd = abs($deg) + $min/60 + $sec/3600;
230             $dd *= $deg / abs($deg) if $dd;
231             return $dd;
232             }
233              
234             ###########################################################################
235             # minTimeStamp and maxTimeStamp return the pt that has the earliest
236             # and latest non-null time stamps.
237              
238             # calcMinMaxTimeStamp -> based on $self->{dirty} calculates
239             # $self->{minTimeStamp} and $self->{maxTimeStamp}. So you can call
240             # minTimeStamp and maxTimeStamp as often as you wish, only the first call
241             # takes any real processing. (and that is just a simple array traversal.)
242             sub calcMinMaxTimeStamp {
243             my $self=shift;
244              
245             $self->{maxTimeStamp} = {timestamp => ""};
246             $self->{minTimeStamp} = {timestamp => ""};
247              
248             foreach my $pt (@{$self->{log}} ) {
249              
250             # shrink it...
251             my $ts = $pt->{timestamp};
252              
253             # valid timestamp? pretty weak test... just yy-
254             next unless ($ts =~ m|\d\d-|);
255            
256             # do we have a min? either we don't have a min, so
257             # use this one, or this one is the same or less then
258             # our current min.
259             $self->{minTimeStamp} = $pt if ( ($ts le $self->{minTimeStamp}->{timestamp})
260             or (! $self->{minTimeStamp}->{timestamp}) );
261              
262             $self->{maxTimeStamp} = $pt if ( ($ts gt $self->{maxTimeStamp}->{timestamp})
263             or (! $self->{maxTimeStamp}->{timestamp}) );
264            
265             }
266             #print "min in calc" . Dumper($self->{minTimeStamp});
267             #print "max in calc" . Dumper($self->{maxTimeStamp});
268             $self->{dirty}=0;
269             }
270              
271             ###########################################################################
272             sub minTimeStamp{
273             my $self = shift;
274             if ($self->{dirty}) {
275             $self->calcMinMaxTimeStamp();
276             }
277             return $self->{minTimeStamp};
278             }
279              
280             ###########################################################################
281             sub maxTimeStamp{
282             my $self = shift;
283             if ($self->{dirty}) {
284             $self->calcMinMaxTimeStamp();
285             }
286             return $self->{maxTimeStamp};
287             }
288              
289              
290             ###########################################################################
291             # whereWasI() - accept a timestamp in the same format and timezone as our
292             # track log, and try and determine where we were...
293             sub whereWasI {
294             my ($self, $d) = @_;
295             my $sPt = $self->minTimeStamp();
296             my $ePt = $self->maxTimeStamp();
297              
298             #
299             # TODO: make this routine not suck to find the 1 or 2 points needed to
300             # interpolate the position
301              
302             # get start point
303             foreach my $pt (@{$self->{log}} ) {
304             $sPt = $pt if ( $d ge $pt->{timestamp} );
305             $ePt = $pt;
306             last if ($d lt $pt->{timestamp});
307             }
308            
309             # what percentage of the way between $sPt->{timestamp} and $ePt->{timestamp} is $d?
310             my $pct = $self->getPercent($d, $sPt, $ePt);
311              
312             # What we know:
313             # $sPt->{lat}
314             # $sPt->{long}
315             # $ePt->{lat}
316             # $ePt->{long}
317             # $pct = the percentage of the way we pass from sPt to ePt for our point.
318              
319             # load pt $pt with the interpolated lat, long, elevation, and ?
320             my $pt;
321            
322             $pt->{lat} = sprintf "%.6f",
323             $sPt->{lat} + ($ePt->{lat} - $sPt->{lat}) * $pct;
324              
325             $pt->{long} = sprintf "%.6f",
326             $sPt->{long} + ($ePt->{long} - $sPt->{long}) * $pct;
327              
328             $pt->{elevation} = sprintf "%.1f",
329             ($sPt->{elevation} + $ePt->{elevation}) / 2;
330              
331             $pt->{timestamp} = $d;
332             $pt->{pct} = $pct;
333              
334             return ($pt, $sPt, $ePt);
335             }
336              
337             # a synonym for whereWasI
338             *interpolate = \&whereWasI;
339              
340             sub getPercent {
341             my ($self, $d, $sPt, $ePt) = @_;
342             my $st = Time::Piece->strptime( $sPt->{timestamp}, "%Y-%m-%d %H:%M:%S" );
343             my $et = Time::Piece->strptime( $ePt->{timestamp}, "%Y-%m-%d %H:%M:%S" );
344             my $dt = Time::Piece->strptime( $d, "%Y-%m-%d %H:%M:%S" );
345              
346             return 0 unless $st and $et and $dt;
347              
348             my $fulldiff = $et->epoch - $st->epoch;
349             my $pct = ($dt->epoch - $st->epoch)/$fulldiff;
350             return $pct;
351             }
352              
353              
354             # accept a ref to an array Geo::Track::Log objects and
355             # then return the distinct union of all of them.
356             #
357             # note: This method seems to work fine, but it doesn't have tests (ack!)
358             # it isn't documented, and it has development comments within...
359             sub combine_waypoint{
360             my ($self, $log_list) = @_;
361              
362              
363             # this doesn't really work yet. In fact, fixGarnixWayLine seems to
364             # not really work in all cases.
365              
366             # hash of hashes key = name, value = a hashref representing a point
367             my %list;
368              
369             # this only works for 'waypoints' which for this are defined
370             # as Geo::Track::Log objects that contain a name field.
371              
372             # I don't have a way to address points by name or identifier.
373             foreach my $log (@$log_list) {
374             #print "$log->{name}\n";
375             foreach my $pt (@{$log->{log}}){
376             # I want something like this, but I can't
377             # have it because I can't address points by name
378             # or identifier, and so this requires a complete
379             # walk of the list of points for every point added.
380             #$self->addPointNonDupe($pt);
381              
382             # add this point to list unless it is a dupe
383            
384             # just add the point and see what happens...
385             # this logic means don't add duplicate names, but
386             # that means we lose points if we have name collisions.
387             # my 'home' today and my home 'tomorrow' have the same
388             # name, but are different points.
389              
390             my $add = 0;
391             # if I used some hash of values in the hash this would
392             # be trivial...
393             if (! $list{$pt->{name}}){
394             # we don't have this point in our list at all
395             $add = 1;
396             #$list{$pt->{name}} = $pt;
397             } else {
398             # we have one point in the list with this name, but
399             # perhaps this point has different information. Say
400             # the same name but a different lat,long, or a different
401             # date or comment.
402            
403             # need to compare this pt with each point already
404             # in the array pointed to by $list{name}
405            
406             # add = 0. Do we want to add this?
407             # only if it Compare() == 0 for all points.
408             my $flag;
409             foreach my $oldpt (@{$list{$pt->{name}}}){
410             # is it different ?
411             $flag += Compare( $oldpt, $pt);
412             # what about distance?
413             # if the names are the same, the CRTD field
414             # is the same, and the lat and long are the
415             # same to 4 decimal places then let it be.
416             if ($pt->{CRTD} eq $oldpt->{CRTD}) {
417             # do something with distance?
418              
419             # assume it is a dupe
420             my $dupe = 1;
421             # this needs code to determine if it is a dupe!
422            
423             }
424             }
425            
426             $add = ! $flag;
427             }
428            
429             if ($add) {
430             push @{$list{$pt->{name}}}, $pt;
431             }
432             }
433             }
434             foreach my $k (sort keys %list) {
435             foreach my $pt (@{$list{$k}}) {
436             $self->addPoint($pt);
437             }
438             }
439            
440             }
441              
442              
443              
444             1;
445              
446              
447             __END__