File Coverage

blib/lib/Geo/Spline.pm
Criterion Covered Total %
statement 89 89 100.0
branch 12 14 85.7
condition 2 3 66.6
subroutine 12 12 100.0
pod 7 7 100.0
total 122 125 97.6


line stmt bran cond sub pod time code
1             package Geo::Spline;
2 1     1   145429 use strict;
  1         3  
  1         47  
3 1     1   7 use warnings;
  1         2  
  1         66  
4 1     1   766 use Geo::Ellipsoids;
  1         5774  
  1         45  
5 1     1   7 use Geo::Constants qw{PI};
  1         6  
  1         59  
6 1     1   6 use Geo::Functions qw{deg_rad rad_deg round};
  1         2  
  1         1459  
7              
8             our $VERSION = '0.17';
9              
10             =head1 NAME
11              
12             Geo::Spline - Calculate geographic locations between GPS fixes.
13              
14             =head1 SYNOPSIS
15              
16             use Geo::Spline;
17             my $p0 = {
18             time => 1160449100.67, #seconds
19             lat => 39.197807, #degrees
20             lon => -77.263510, #degrees
21             speed => 31.124, #m/s
22             heading => 144.8300, #degrees clockwise from North
23             };
24             my $p1 = {
25             time => 1160449225.66,
26             lat => 39.167718,
27             lon => -77.242278,
28             speed => 30.615,
29             heading =>1 50.5300,
30             };
31             my $spline = Geo::Spline->new($p0, $p1);
32             my %point = $spline->point(1160449150);
33             print "Lat:", $point{"lat"}, ", Lon:", $point{"lon"}, "\n\n";
34              
35             my @points = $spline->pointlist();
36             foreach (@points) {
37             print "Lat:", $_->{"lat"}, ", Lon:", $_->{"lon"}, "\n";
38             }
39              
40             =head1 DESCRIPTION
41              
42             This program was developed to be able to calculate the position between two GPS fixes using a 2-dimensional 3rd order polynomial spline.
43              
44             f(t) = A + B(t-t0) + C(t-t0)^2 + D(t-t0)^3 #position in X and Y
45             f'(t) = B + 2C(t-t0) + 3D(t-t0)^2 #velocity in X and Y
46              
47             Formulas to calculate the unknowns from our knowns.
48              
49             A = x0 # when (t-t0)=0 in f(t)
50             B = v0 # when (t-t0)=0 in f'(t)
51             C = (x1-A-B(t1-t0)-D(t1-t0)^3)/(t1-t0)^2 # solve for C from f(t)
52             C = (v1-B-3D(t1-t0)^2)/2(t1-t0) # solve for C from f'(t)
53             D = (v1(t1-t0)+B(t1-t0)-2x1+2A)/(t1-t0)^3 # equate C=C then solve for D
54              
55             =head1 CONSTRUCTOR
56              
57             =head2 new
58              
59             my $spline = Geo::Spline->new($p0, $p1);
60              
61             =cut
62              
63             sub new {
64 1     1 1 234363 my $this = shift;
65 1 50       6 my $class = ref($this) ? ref($this) : $this;
66 1         3 my $self = {};
67 1         4 bless $self, $class;
68 1         10 $self->initialize(@_);
69 1         4 return $self;
70             }
71              
72             =head2 initialize
73              
74             =cut
75              
76             sub initialize {
77 1     1 1 2 my $self = shift();
78 1         6 $self->{'pt0'} = shift();
79 1         3 $self->{'pt1'} = shift();
80 1         6 my $ellipsoid = $self->ellipsoid('WGS84');
81 1         4 my $dt = $self->{'pt1'}->{'time'} - $self->{'pt0'}->{'time'};
82 1 50       6 die ('Delta time must be greater than zero.') if ($dt<=0);
83             my ($A, $B, $C, $D) = $self->ABCD(
84             $self->{'pt0'}->{'time'},
85             $self->{'pt0'}->{'lat'} * $ellipsoid->polar_circumference / 360,
86             $self->{'pt0'}->{'speed'} * cos(rad_deg($self->{'pt0'}->{'heading'})),
87             $self->{'pt1'}->{'time'},
88             $self->{'pt1'}->{'lat'} * $ellipsoid->polar_circumference / 360,
89 1         6 $self->{'pt1'}->{'speed'} * cos(rad_deg($self->{'pt1'}->{'heading'})));
90 1         4 $self->{'Alat'} = $A;
91 1         3 $self->{'Blat'} = $B;
92 1         49 $self->{'Clat'} = $C;
93 1         5 $self->{'Dlat'} = $D;
94             ($A, $B, $C, $D) = $self->ABCD(
95             $self->{'pt0'}->{'time'},
96             $self->{'pt0'}->{'lon'} * $ellipsoid->equatorial_circumference / 360,
97             $self->{'pt0'}->{'speed'} * sin(rad_deg($self->{'pt0'}->{'heading'})),
98             $self->{'pt1'}->{'time'},
99             $self->{'pt1'}->{'lon'} * $ellipsoid->equatorial_circumference / 360,
100 1         6 $self->{'pt1'}->{'speed'} * sin(rad_deg($self->{'pt1'}->{'heading'})));
101 1         20 $self->{'Alon'} = $A;
102 1         4 $self->{'Blon'} = $B;
103 1         3 $self->{'Clon'} = $C;
104 1         5 $self->{'Dlon'} = $D;
105             }
106              
107             =head1 METHODS
108              
109             =head2 ellipsoid
110              
111             Method to set or retrieve the current ellipsoid object. The ellipsoid is a Geo::Ellipsoids object.
112              
113             my $ellipsoid=$obj->ellipsoid; #Default is WGS84
114              
115             $obj->ellipsoid('Clarke 1866'); #Built in ellipsoids from Geo::Ellipsoids
116             $obj->ellipsoid({a=>1}); #Custom Sphere 1 unit radius
117              
118             =cut
119              
120             sub ellipsoid {
121 1259     1259 1 5271 my $self = shift();
122 1259 100       4389 if (@_) {
123 1         3 my $param = shift();
124 1         9 my $obj = Geo::Ellipsoids->new($param);
125 1         330 $self->{'ellipsoid'} = $obj;
126             }
127 1259         2471 return $self->{'ellipsoid'};
128             }
129              
130             =head2 ABCD
131              
132             =cut
133              
134             sub ABCD {
135 2     2 1 111 my $self = shift();
136 2         4 my $t0 = shift();
137 2         3 my $x0 = shift();
138 2         5 my $v0 = shift();
139 2         3 my $t1 = shift();
140 2         4 my $x1 = shift();
141 2         4 my $v1 = shift();
142             #x=f(t)=A+B(t-t0)+C(t-t0)^2+D(t-t0)^3
143             #v=f'(t)=B+2C(t-t0)+3D(t-t0)^2
144             #A=x0
145             #B=v0
146             #C=(x1-A-B(t1-t0)-D(t1-t0)^3)/((t1-t0)^2) # from f(t)
147             #C=(v1-B-3D(t1-t0)^2)/2(t1-t0) # from f'(t)
148             #D=(v1t+Bt-2x1+2A)/t^3 # from C=C
149 2         10 my $A = $x0;
150 2         4 my $B = $v0;
151             #=(C3*(A3-A2)+B6*(A3-A2)-2*B3+2*B5)/(A3-A2)^3 # for Excel
152 2         11 my $D = ($v1*($t1-$t0)+$B*($t1-$t0)-2*$x1+2*$A)/($t1-$t0)**3;
153             #=(B3-B5-B6*(A3-A2)-B8*(A3-A2)^3)/(A3-A2)^2 # for Excel
154 2         8 my $C = ($x1-$A-$B*($t1-$t0)-$D*($t1-$t0)**3)/($t1-$t0)**2;
155 2         8 return($A,$B,$C,$D);
156             }
157              
158             =head2 point
159              
160             Method returns a single point from a single time.
161              
162             my $point = $spline->point($t1);
163             my %point = $spline->point($t1);
164              
165             =cut
166              
167             sub point {
168 1258     1258 1 17984 my $self = shift();
169 1258         1941 my $timereal = shift();
170 1258         2665 my $ellipsoid = $self->ellipsoid;
171 1258         2612 my $t = $timereal-$self->{'pt0'}->{'time'};
172 1258         3383 my ($Alat, $Blat, $Clat, $Dlat)=($self->{'Alat'}, $self->{'Blat'},$self->{'Clat'},$self->{'Dlat'});
173 1258         3011 my ($Alon, $Blon, $Clon, $Dlon)=($self->{'Alon'}, $self->{'Blon'},$self->{'Clon'},$self->{'Dlon'});
174 1258         3329 my $lat = $Alat + $Blat * $t + $Clat * $t ** 2 + $Dlat * $t ** 3;
175 1258         2557 my $lon = $Alon + $Blon * $t + $Clon * $t ** 2 + $Dlon * $t ** 3;
176 1258         2432 my $vlat = $Blat + 2 * $Clat * $t + 3 * $Dlat * $t ** 2;
177 1258         2318 my $vlon = $Blon + 2 * $Clon * $t + 3 * $Dlon * $t ** 2;
178 1258         2449 my $speed = sqrt($vlat ** 2 + $vlon ** 2);
179 1258         2802 my $heading = PI()/2 - atan2($vlat,$vlon);
180 1258         6719 $heading = deg_rad($heading);
181 1258         25749 $lat /= $ellipsoid->polar_circumference / 360;
182 1258         16871 $lon /= $ellipsoid->equatorial_circumference / 360;
183 1258         17595 my %pt = (
184             time => $timereal,
185             lat => $lat,
186             lon => $lon,
187             speed => $speed,
188             heading => $heading,
189             );
190 1258 100       10535 return wantarray ? %pt : \%pt;
191             }
192              
193             =head2 pointlist
194              
195             Method returns a list of points from a list of times.
196              
197             my $list = $spline->pointlist($t1,$t2,$t3);
198             my @list = $spline->pointlist($t1,$t2,$t3);
199              
200             =cut
201              
202             sub pointlist {
203 4     4 1 11747 my $self = shift();
204 4         64 my @list = @_;
205 4 100       23 @list = $self->timelist() if (scalar(@list)== 0);
206 4         11 my @points = ();
207 4         11 foreach (@list) {
208 1256         15431 push @points, {$self->point($_)};
209             }
210 4 100       383 return wantarray ? @points : \@points;
211             }
212              
213             =head2 timelist
214              
215             Method returns a list of times (n+1). The default will return a list with an integer number of seconds between spline end points.
216              
217             my $list = $spline->timelist($samples);
218             my @list = $spline->timelist();
219              
220             =cut
221              
222             sub timelist {
223 6     6 1 11122 my $self = shift();
224 6         21 my $t0 = $self->{'pt0'}->{'time'};
225 6         22 my $t1 = $self->{'pt1'}->{'time'};
226 6         11 my $dt = $t1-$t0;
227 6   66     34 my $count = shift() || round($dt);
228 6         64 my @list;
229 6         23 foreach(0 .. $count) {
230 1506         2418 my $t = $t0 + $dt*($_/$count);
231 1506         2708 push @list, $t;
232             }
233 6 100       242 return wantarray ? @list : \@list;
234             }
235              
236             =head1 LIMITATIONS
237              
238             The conversion from degrees to meters and then back is accurate for short distances.
239              
240             =head1 AUTHOR
241              
242             Michael R. Davis
243              
244             =head1 LICENSE
245              
246             Copyright (c) 2006-2025 Michael R. Davis
247              
248             This library is free software; you can redistribute it and/or modify
249             it under the same terms as Perl itself.
250              
251             =head1 SEE ALSO
252              
253             http://search.cpan.org/src/MRDVT/Geo-Spline-0.16/doc/spline.xls
254             http://search.cpan.org/src/MRDVT/Geo-Spline-0.16/doc/spline.png
255              
256             L, L
257              
258             =cut
259              
260             1;