File Coverage

blib/lib/Geo/CheapRuler.pm
Criterion Covered Total %
statement 14 251 5.5
branch 0 44 0.0
condition 0 30 0.0
subroutine 5 25 20.0
pod 18 19 94.7
total 37 369 10.0


line stmt bran cond sub pod time code
1             # ABSTRACT: Geo::CheapRuler Fast Geodesic (Lat,Lon) approximations for city scale / not near the poles. Port of mapbox/cheap-ruler.
2              
3             package Geo::CheapRuler;
4              
5             #
6             # how to update README.md
7             # pod2markdown CheapRuler.pm > ../../README.md
8             #
9             # $VERSION appears twice here, and once on GITHUB
10             #
11              
12             our $VERSION = 'v0.1.1';
13              
14             =head1 NAME
15              
16             Geo::CheapRuler - Fast GPS Geodesic functions, port of mapbox/cheap-ruler
17              
18             =head1 VERSION
19              
20             v0.1.1
21              
22             =head1 SYNOPSIS
23              
24             A collection of very fast approximations to common geodesic measurements. Useful for performance-sensitive code that measures things on a city scale (less than 500km, not near the poles).
25             Can be an order of magnitude faster than Haversine based methods.
26              
27             A Perl port of Mapbox's cheap-ruler v4.0.0 https://github.com/mapbox/cheap-ruler
28              
29             Very fast as they use just 1 trig function per call.
30              
31             =head1 MATHS MODEL
32              
33             The Maths model is based upon an approximation to Vincenty's formulae, which uses the Earth's actual shape, an oblate ellipsoid (squashed sphere). For 'city' scale work, it is still more accurate than
34             the Haversine formulae (which uses several trig calls based upon a spherical Earth). For an explanation, see
35             https://blog.mapbox.com/fast-geodesic-approximations-with-cheap-ruler-106f229ad016
36              
37             =head1 EXPORT
38              
39             Nothing
40              
41             =head1 WEBSITE
42              
43             https://github.com/aavmurphy/CheapRuler
44              
45             =head1 USAGE
46              
47             This module uses "geojson" style GPS geometery. Points are [lon, lat]. Polygons are a series of rings. The first ring is exterior and clockwise. Subsequent rings are interior (holes) and anticlockwise.
48              
49             The latitude (lat) parameter passed to the constructor should be the 'median' of the lat's used, i.e. (max-lat + min-lat)/2.
50              
51             Some methods have units, e.g. "expand a bounding box by 10 meters/miles/kilometers". The default 'units' are 'kilometers', you may which to use 'meters'.
52              
53             Data is passed / retured as arrayrefs, e.g. $p = [ 0.1, 54.1 ];
54              
55             =head1 EXAMPLES
56              
57             In the examples below, $p is a point, $a and $b are a line segment.
58              
59             $p = [ -1, 57 ];
60              
61             $a = [0.1, 54.1];
62             $b = [0.2, 54.2];
63              
64             $ruler = Cheap::Ruler->new( ( 54.1 + 54.2 )/2, 'meters' );
65             # so the 'units' below are meters
66              
67             $distance = $ruler->distance( $a, $b );
68             # return meters
69              
70             $bearing = $ruler->bearing( $a, $b );
71             # returns degrees
72              
73             $point = $ruler->destination( $a, 1000, 90);
74             # returns a new point, 1000 units away at 90 degrees
75              
76             $point = $ruler->offset( $p, 100, 200 );
77             # returns a point 100 units east, 200 units north
78              
79             $distance = $ruler->lineDistance( [ $p, $a, $b ] );
80             # length of the line
81              
82             $area = $ruler->area( [
83             [-67.031, 50.458], [-67.031, 50.534], [-66.929, 50.534], [-66.929, 50.458], [-67.031, 50.458]
84             ] ); # area of a polygon
85              
86             $point = $ruler->along( [ [-67.031, 50.458], [-67.031, 50.534], [-66.929, 50.534] ], 2.5);
87             # returns a point 2.5 units along the line
88              
89             $distance = $ruler->pointToSegmentDistance( $p, $a, $b );
90             # distance from point to a 2 point line segment
91              
92             =cut
93              
94 1     1   84574 use strict;
  1         2  
  1         31  
95 1     1   3 use warnings;
  1         1  
  1         39  
96 1     1   11 use v5.20; # min version for experimental signatures
  1         2  
97 1     1   429 use experimental 'signatures';
  1         2999  
  1         5  
98 1     1   811 use Math::Trig;
  1         12264  
  1         2576  
99             #use Data::Dumper;
100              
101             our %FACTORS = (
102             kilometers => 1,
103             miles => 1000 / 1609.344,
104             nauticalmiles => 1000 / 1852,
105             meters => 1000,
106             metres => 1000,
107             yards => 1000 / 0.9144,
108             feet => 1000 / 0.3048,
109             inches => 1000 / 0.0254
110             );
111              
112             # Values that define WGS84 ellipsoid model of the Earth
113              
114             our $RE = 6378.137; # equatorial radius
115             our $FE = 1 / 298.257223563; # flattening
116              
117             our $E2 = $FE * (2 - $FE);
118             our $RAD = pi / 180; # pi from Math::Trig
119              
120             #
121             # A collection of very fast approximations to common geodesic measurements. Useful for performance-sensitive code that measures things on a city scale.
122             #
123              
124             =head1 API
125              
126             =head2 CheapRuler::fromTile( $y, $z, $units='kilometers' )
127              
128             Creates a ruler object from Google web mercator tile coordinates (y and z). That's correct, y and z, not x.
129              
130             See 'new' below for available units.
131              
132             Example
133              
134             $ruler = CheapRuler::fromTile( 11041, 15, 'meters');
135              
136             =cut
137              
138 0     0 1   sub fromTile( $y, $z, $units='kilometers') {
  0            
  0            
  0            
  0            
139 0           my $n = pi * (1 - 2 * ($y + 0.5) / ( 2**$z ));
140 0           my $lat = atan(0.5 * ( exp($n) - exp(-$n) )) / $RAD;
141              
142 0           return CheapRuler->new($lat, $units);
143             }
144              
145             =head2 units()
146              
147             Multipliers for converting between units.
148            
149             See 'new' below for available units.
150              
151             Example : convert 50 meters to yards
152              
153             $units = CheapRuler::units();
154              
155             $yards = 50 * $units->{yards} / $units->{meters};
156              
157             =cut
158              
159 0     0     sub CheapRuler::units() {
  0            
160 0           return { %FACTORS };
161             }
162              
163             =head2 CheapRuler->new( $lat, $units='kilometers' )
164              
165             Create a ruler instance for very fast approximations to common geodesic measurements around a certain latitude.
166              
167             param latitude, e.g. 54.31
168              
169             param units (optional), one of: kilometers miles nauticalmiles meters metres yards feet inches
170            
171             Example
172              
173             $ruler = CheapRuler->new(35.05, 'miles');
174              
175             =cut
176              
177 0     0 1   sub new ( $class, $lat, $units='kilometers' ) {
  0            
  0            
  0            
  0            
178 0 0         croak( 'No latitude given.') if ! defined $lat;
179 0 0 0       croak( "Unknown unit $units. Use one of: " + join( ', ', keys(%FACTORS) ) ) if $units && ! $FACTORS{ $units };
180              
181 0           my $self = bless {};
182              
183             # Curvature formulas from https://en.wikipedia.org/wiki/Earth_radius#Meridional
184 0 0         my $m = $RAD * $RE * ( $units ? $FACTORS{ $units } : 1 );
185 0           my $coslat = cos( $lat * $RAD );
186 0           my $w2 = 1 / (1 - $E2 * (1 - $coslat**2) );
187 0           my $w = sqrt($w2);
188              
189             # multipliers for converting longitude and latitude degrees into distance
190 0           $self->{kx} = $m * $w * $coslat; # based on normal radius of curvature
191 0           $self->{ky} = $m * $w * $w2 * (1 - $E2); # based on meridonal radius of curvature
192            
193 0           return $self;
194             }
195              
196             =head2 distance( $a, $b )
197              
198             Given two points of the form [longitude, latitude], returns the distance in 'ruler' units.
199              
200             param $a, point [longitude, latitude]
201              
202             param $b, point [longitude, latitude]
203              
204             returns distance (in chosen units)
205              
206             Example
207              
208             $distance = $ruler->distance([30.5, 50.5], [30.51, 50.49]);
209              
210             =cut
211              
212 0     0 1   sub distance( $self, $a, $b) {
  0            
  0            
  0            
  0            
213              
214 0           my $dx = &wrap( $a->[0] - $b->[0]) * $self->{kx};
215 0           my $dy = ( $a->[1] - $b->[1]) * $self->{ky};
216 0           return sqrt( $dx * $dx + $dy * $dy);
217             }
218              
219             =head2 bearing( $a, $b )
220              
221             Returns the bearing between two points in degrees
222            
223             param $a, point [longitude, latitude]
224              
225             param $b, point [longitude, latitude]
226              
227             returns $bearing (degrees)
228              
229             Example
230              
231             $bearing = $ruler->bearing([30.5, 50.5], [30.51, 50.49]);
232              
233             =cut
234              
235 0     0 1   sub bearing($self, $a, $b) {
  0            
  0            
  0            
  0            
236 0           my $dx = &wrap($b->[0] - $a->[0]) * $self->{kx};
237 0           my $dy = ($b->[1] - $a->[1]) * $self->{ky};
238 0           return atan2($dx, $dy) / $RAD;
239             }
240              
241             =head2 destination( $point, $distance, $bearing)
242              
243             Returns a new point given distance and bearing from the starting point.
244              
245             param $p point [longitude, latitude]
246              
247             param $dist distance in chosen units
248              
249             param $bearing (degrees)
250              
251             returns $point [longitude, latitude]
252            
253             Example
254              
255             $point = ruler->destination([30.5, 50.5], 0.1, 90);
256              
257             =cut
258              
259 0     0 1   sub destination( $self, $p, $dist, $bearing) {
  0            
  0            
  0            
  0            
  0            
260 0           my $a = $bearing * $RAD;
261 0           return $self->offset(
262             $p,
263             sin($a) * $dist,
264             cos($a) * $dist,
265             );
266             }
267              
268             =head2 offset( $point, dx, dy )
269            
270             Returns a new point given easting and northing offsets (in ruler units) from the starting point.
271            
272             param $point, [longitude, latitude]
273              
274             param $dx, easting, in ruler units
275              
276             param $dy, northing, in ruler units
277              
278             returns $point [longitude, latitude]
279              
280             Example
281              
282             $point = ruler.offset([30.5, 50.5], 10, 10);
283             =cut
284              
285 0     0 1   sub offset( $self, $p, $dx, $dy) {
  0            
  0            
  0            
  0            
  0            
286             return [
287             $p->[0] + $dx / $self->{kx},
288             $p->[1] + $dy / $self->{ky},
289 0           ];
290             }
291            
292             =head2 lineDistance ( $points )
293              
294             Given a line (an array of points), returns the total line distance.
295              
296             param $points, listref of points, where a point is [longitude, latitude]
297              
298             returns $number, total line distance in 'ruler' units
299              
300             Example
301              
302             $length = ruler->lineDistance([
303             [-67.031, 50.458], [-67.031, 50.534],
304             [-66.929, 50.534], [-66.929, 50.458]
305             ]);
306             =cut
307              
308 0     0 1   sub lineDistance( $self, $points ) {
  0            
  0            
  0            
309 0           my $total = 0;
310 0           foreach my $i ( 0 .. $#{ $points } - 1 ) {
  0            
311 0           $total += $self->distance( $points->[ $i ], $points->[ $i + 1 ] );
312             }
313 0           return $total;
314             }
315              
316             =head2 area( $polygon )
317              
318             Given a polygon (an array of rings, where each ring is an array of points), returns the area.
319            
320             param $polygon, a list-ref of rings, where a ring is a list of points [lon,lat], 1st ring is outer, 2nd+ rings are inner (holes)
321              
322             returns $number, area value in the specified 'ruler' units (square kilometers by default)
323              
324             Example
325              
326             $area = $ruler->area([[
327             [-67.031, 50.458], [-67.031, 50.534], [-66.929, 50.534], [-66.929, 50.458], [-67.031, 50.458]
328             ]]);
329             =cut
330              
331 0     0 1   sub area( $self, $polygon ) {
  0            
  0            
  0            
332 0           my $sum = 0;
333              
334 0           foreach my $i ( 0 .. $#{ $polygon } ) {
  0            
335 0           my @ring = @{ $polygon->[ $i ] };
  0            
336            
337 0           for ( my ( $j, $len, $k ) = ( 0, scalar( @ring ), $#ring ); $j < $len; $k = $j++) {
338 0 0         $sum += &wrap( $ring[$j]->[0] - $ring[$k]->[0]) * ( $ring[$j]->[1] + $ring[$k]->[1]) * ( $i ? -1 : 1 );
339             }
340             }
341              
342 0           return ( abs( $sum ) / 2 ) * $self->{kx} * $self->{ky};
343             }
344              
345             =head2 along( $line, $distance)
346              
347             Returns the point at a specified distance along the line.
348              
349             param $line, a list-ref of points of [lon, lat]
350              
351             param $dist, distance in ruler units
352              
353             returns $point, a list-ref [lon, lat]
354              
355             Example
356              
357             $point = $ruler->along(
358             [ [-67.031, 50.458], [-67.031, 50.534], [-66.929, 50.534] ],
359             2.5);
360             =cut
361              
362 0     0 1   sub along( $self, $line, $dist ) {
  0            
  0            
  0            
  0            
363 0           my $sum = 0;
364              
365 0 0         return $line->[0] if $dist <= 0 ;
366              
367 0           for (my $i = 0; $i < $#{ $line }; $i++) {
  0            
368 0           my $p0 = $line->[$i];
369 0           my $p1 = $line->[$i + 1];
370 0           my $d = $self->distance($p0, $p1);
371 0           $sum += $d;
372 0 0         return &interpolate( $p0, $p1, ( $dist - ($sum - $d)) / $d ) if $sum > $dist;
373             }
374              
375 0           return $line->[ $#{ $line } ];
  0            
376             }
377              
378             =head2 pointToSegmentDistance( $p, $a, $b )
379              
380             Returns the distance from a point `p` to a line segment `a` to `b`.
381              
382             param $p, point, [longitude, latitude]
383              
384             param $a, segment point 1, [longitude, latitude]
385              
386             param $b, segment point 2, [longitude, latitude]
387              
388             returns $distance (in ruler units)
389            
390             Example
391              
392             $distance = $ruler->pointToSegmentDistance([-67.04, 50.5], [-67.05, 50.57], [-67.03, 50.54]);
393              
394             =cut
395              
396 0     0 1   sub pointToSegmentDistance( $self, $p, $a, $b) {
  0            
  0            
  0            
  0            
  0            
397 0           my $x = $a->[0];
398 0           my $y = $a->[1];
399 0           my $dx = &wrap( $b->[0] - $x) * $self->{kx};
400 0           my $dy = ($b->[1] - $y) * $self->{ky};
401              
402 0 0 0       if ( $dx != 0 || $dy != 0) {
403 0           my $t = ( &wrap( $p->[0] - $x) * $self->{kx} * $dx + ( $p->[1] - $y) * $self->{ky} * $dy) / ($dx**2 + $dy**2);
404              
405 0 0         if ($t > 1) {
    0          
406 0           $x = $b->[0];
407 0           $y = $b->[1];
408             }
409             elsif ( $t > 0) {
410 0           $x += ($dx / $self->{kx}) * $t;
411 0           $y += ($dy / $self->{ky}) * $t;
412             }
413             }
414              
415 0           $dx = &wrap($p->[0] - $x) * $self->{kx};
416 0           $dy = ($p->[1] - $y) * $self->{ky};
417              
418 0           return sqrt($dx**2 + $dy**2);
419             }
420              
421             =head2 pointOnLine( $line, $p )
422              
423             Returns an object of the form {point, index, t}, where
424              
425             * point is closest point on the line from the given point,
426              
427             * index is the start index of the segment with the closest point,
428              
429             * t is a parameter from 0 to 1 that indicates where the closest point is on that segment.
430              
431              
432             param $line, listref of points of [lon, lat]
433              
434             param $p, point of [longitude, latitude]
435              
436             returns { point => [lon, lat], index => number, t => number }
437              
438             Example
439              
440             $info = $ruler->pointOnLine( $line, [-67.04, 50.5])
441              
442             =cut
443              
444 0     0 1   sub pointOnLine( $self, $line, $p) {
  0            
  0            
  0            
  0            
445 0           my $minDist = "infinity";
446 0           my $minX = $line->[0][0];
447 0           my $minY = $line->[0][1];
448 0           my $minI = 0;
449 0           my $minT = 0;
450              
451 0           for ( my $i = 0; $i < $#{ $line }; $i++) {
  0            
452              
453 0           my $x = $line->[$i][0];
454 0           my $y = $line->[$i][1];
455 0           my $dx = &wrap( $line->[$i + 1][0] - $x) * $self->{kx};
456 0           my $dy = ($line->[$i + 1][1] - $y) * $self->{ky};
457 0           my $t = 0;
458              
459 0 0 0       if ($dx != 0 || $dy != 0) {
460 0           $t = ( &wrap($p->[0] - $x) * $self->{kx} * $dx + ($p->[1] - $y) * $self->{ky} * $dy) / ($dx**2 + $dy**2);
461              
462 0 0         if ($t > 1) {
    0          
463 0           $x = $line->[$i + 1][0];
464 0           $y = $line->[$i + 1][1];
465              
466             } elsif ($t > 0) {
467 0           $x += ($dx / $self->{kx}) * $t;
468 0           $y += ($dy / $self->{ky}) * $t;
469             }
470             }
471              
472 0           $dx = wrap($p->[0] - $x) * $self->{kx};
473 0           $dy = ($p->[1] - $y) * $self->{ky};
474              
475 0           my $sqDist = $dx**2 + $dy**2;
476 0 0 0       if ( $minDist eq 'infinity' || $sqDist < $minDist) {
477 0           $minDist = $sqDist;
478 0           $minX = $x;
479 0           $minY = $y;
480 0           $minI = $i;
481 0           $minT = $t;
482             }
483             }
484              
485             # Math::max(0, Math::min(1, $minT));
486 0 0         my $min = 1 < $minT ? 1 : $minT;
487 0 0         my $max = 0 > $min ? 0 : $min;
488              
489             return {
490 0           point => [$minX,$minY],
491             index => $minI,
492             t => $max,
493             };
494             }
495              
496             =head2 lineSlice( $start, $stop, $line )
497              
498             Returns a part of the given line between the start and the stop points (or their closest points on the line).
499              
500             param $start, point [longitude, latitude]
501              
502             param $stop, point [longitude, latitude]
503              
504             param $line, arrayref of points of [lon,lat]
505              
506             returns $linea_slice (a listref) part of the line
507              
508             Example
509              
510             $line_slice = $ruler->lineSlice([-67.04, 50.5], [-67.05, 50.56], $line);
511              
512             =cut
513              
514 0     0 1   sub lineSlice($self, $start, $stop, $line) {
  0            
  0            
  0            
  0            
  0            
515 0           my $p1 = $self->pointOnLine($line,$start);
516 0           my $p2 = $self->pointOnLine($line,$stop);
517              
518 0 0 0       if ( $p1->{index} > $p2->{index} || ( $p1->{index} == $p2->{index} && $p1->{t} > $p2->{t} )) {
      0        
519 0           my $tmp = $p1;
520 0           $p1 = $p2;
521 0           $p2 = $tmp;
522             }
523              
524 0           my @slice = ( $p1->{point} );
525              
526 0           my $l = $p1->{index} + 1;
527 0           my $r = $p2->{index};
528            
529 0 0 0       if ( ( ! &equals( $line->[1], $slice[0]) ) && ( $l <= $r ) ) {
530 0           push @slice, $line->[$l];
531             }
532              
533 0           for (my $i = $l + 1; $i <= $r; $i++) {
534 0           push @slice, $line->[$i];
535             }
536              
537 0 0         if ( ! &equals( $line->[$r], $p2->{point} )) {
538 0           push @slice, $p2->{point};
539             }
540              
541 0           return [ @slice ];
542             }
543              
544             =head2 lineSliceAlong( $start, $stop, $line )
545              
546             Returns a part of the given line between the start and the stop points indicated by distance (in 'units') along the line.
547              
548             param $start, distance, in ruler units
549              
550             param $stop stop, distance, in ruler units
551              
552             param $line, listref of points
553              
554             returns $line_slice, listref of points, part of a line
555              
556             Example
557              
558             $line_slice = $ruler->lineSliceAlong(10, 20, $line);
559              
560             =cut
561              
562 0     0 1   sub lineSliceAlong( $self, $start, $stop, $line) {
  0            
  0            
  0            
  0            
  0            
563 0           my $sum = 0;
564 0           my @slice = ();
565              
566 0           for (my $i = 0; $i < $#{ $line }; $i++) {
  0            
567 0           my $p0 = $line->[$i];
568 0           my $p1 = $line->[$i + 1];
569 0           my $d = $self->distance($p0, $p1);
570              
571 0           $sum += $d;
572              
573 0 0 0       if ($sum > $start && ( scalar @slice ) == 0) {
574 0           push @slice, &interpolate($p0, $p1, ($start - ($sum - $d)) / $d);
575             }
576              
577 0 0         if ($sum >= $stop) {
578 0           push @slice, &interpolate($p0, $p1, ($stop - ($sum - $d)) / $d);
579 0           return [ @slice ];
580             }
581              
582 0 0         if ($sum > $start) { push @slice, $p1 };
  0            
583             }
584              
585 0           return [ @slice ];
586             }
587              
588             =head2 bufferPoint( point, buffer_distance )
589              
590             Given a point, returns a bounding box object ([w, s, e, n]) created from the given point buffered by a given distance.
591            
592             param $p point [longitude, latitude]
593              
594             param $buffer, a distance in ruler units
595              
596             returns $bbox, listref, [w, s, e, n]
597              
598             Example
599              
600             $bbox = $ruler->bufferPoint([30.5, 50.5], 0.01);
601            
602             =cut
603              
604 0     0 1   sub bufferPoint( $self, $p, $buffer) {
  0            
  0            
  0            
  0            
605 0           my $v = $buffer / $self->{ky};
606 0           my $h = $buffer / $self->{kx};
607             return [
608 0           $p->[0] - $h,
609             $p->[1] - $v,
610             $p->[0] + $h,
611             $p->[1] + $v
612             ];
613             }
614              
615             =head2 bufferBBox( $bbox, $buffer )
616              
617             Given a bounding box, returns the box buffered by a given distance.
618              
619             param $bbox, listref of [w, s, e, n]
620              
621             param $buffer, distance in ruler units
622              
623             returns $bbox, a listref, [w, s, e, n]
624              
625             Example
626              
627             $bbox = $ruler->bufferBBox([30.5, 50.5, 31, 51], 0.2);
628              
629             =cut
630              
631 0     0 1   sub bufferBBox( $self, $bbox, $buffer) {
  0            
  0            
  0            
  0            
632 0           my $v = $buffer / $self->{ky};
633 0           my $h = $buffer / $self->{kx};
634             return [
635 0           $bbox->[0] - $h,
636             $bbox->[1] - $v,
637             $bbox->[2] + $h,
638             $bbox->[3] + $v,
639             ];
640             }
641              
642             =head2 insideBBox( $point, $bbox )
643              
644             Returns true (1) if the given point is inside in the given bounding box, otherwise false (0).
645              
646             param $p point [longitude, latitude]
647              
648             param $bbox, listref [w, s, e, n]
649              
650             returns 0 or 1 (boolean)
651              
652             Example
653              
654             $is_inside = $ruler->insideBBox([30.5, 50.5], [30, 50, 31, 51]);
655              
656             =cut
657              
658 0     0 1   sub insideBBox( $self, $p, $bbox) {
  0            
  0            
  0            
  0            
659 0   0       return &wrap( $p->[0] - $bbox->[0]) >= 0 &&
660             &wrap( $p->[0] - $bbox->[2]) <= 0 &&
661             $p->[1] >= $bbox->[1] &&
662             $p->[1] <= $bbox->[3];
663             }
664              
665             =head2 CheapRuler::equals( $a, $b)
666              
667             Tests if 2 points are equal.
668              
669             a function not a method!
670              
671             param $a, point, [ lon, lat ]
672              
673             param $b, point, [ lon, lat ]
674              
675             =cut
676              
677 0     0 1   sub equals($a, $b) {
  0            
  0            
  0            
678 0 0 0       return ( $a->[0] == $b->[0] && $a->[1] == $b->[1] ) ? 1 : 0;
679             }
680              
681             =head2 CheapRuler::interpolate( $a, $b, $t )
682              
683             Returns a point along a line segment from $a to $b
684              
685             a function not a method!
686              
687             param $a, point, [lon, lat]
688              
689             param $b, point, [lon, lat]
690              
691             param $t, ratio (0 <= $t < 1 ), of the way along the line segment
692              
693             returns $p, point [ lon, lat]
694              
695             =cut
696              
697 0     0 1   sub interpolate($a, $b, $t) {
  0            
  0            
  0            
  0            
698 0           my $dx = &wrap($b->[0] - $a->[0]);
699 0           my $dy = $b->[1] - $a->[1];
700             return [
701 0           $a->[0] + $dx * $t,
702             $a->[1] + $dy * $t
703             ];
704             }
705              
706             =head2 CheapRuler::normalize( $degrees )
707              
708             Normalize a lon degree value into [-180..180] range
709              
710             a function not a method!
711              
712             param $degrees
713              
714             return $degrees
715            
716             =cut
717              
718 0     0 0   sub wrap( $deg) {
  0            
  0            
719            
720 0           while ( $deg < -180) { $deg += 360; }
  0            
721 0           while ( $deg > 180) { $deg -= 360; }
  0            
722              
723 0           return $deg;
724             }
725              
726              
727             =head1 SEE ALSO
728              
729             GIS::Distance
730              
731             https://metacpan.org/pod/GIS::Distance
732              
733             Group of modules with XS versions which implement Haversine and Vincenty distance formulas
734              
735             Consider for longer than 'city scale' distances, or near the Poles.
736              
737             GIS::Distance::Vincenty
738              
739             The more accurate formulae which this module approximates. There is an XS version of it.
740              
741             https://metacpan.org/pod/GIS::Distance::Vincenty
742              
743             =head1 SUPPORT
744              
745             You can find documentation for this module with the perldoc command.
746              
747             perldoc Geo::CheapRuler
748              
749             Or at
750              
751             https://github.com/aavmurphy/CheapRuler
752              
753             =head1 BUGS
754              
755             Please report any bugs or feature requests of this port to
756              
757             https://github.com/aavmurphy/CheapRuler
758              
759             For the original, please see
760              
761             https://github.com/mapbox/cheap-ruler
762              
763             For testing, see
764              
765             https://github.com/aavmurphy/CheapRuler/blob/main/test/test.pl
766              
767             =head1 AUTHOR
768              
769             Andrew Murphy, C<< >>
770              
771             =head1 LICENSE AND COPYRIGHT
772              
773             The original, mapbox/cheap-ruler, is (c) Mapbox.
774              
775             This port is Copyright (c) 2025 by Andrew Murphy.
776              
777             This is free software, licensed under:
778              
779             The Artistic License 2.0 (GPL Compatible)
780              
781             =head1 ACKNOWLEDGEMENTS
782              
783             This module is a direct port of mapbox/cheap-ruler
784              
785             =head1 DEVELOPER NOTES
786              
787             README.md is auto-generated from Perl POD
788              
789             The original's test were also ported. See the Github ./tests directory.
790              
791             =cut
792              
793             1; # End of Geo::CheapRuler