File Coverage

lib/Geo/Google/PolylineEncoder.pm
Criterion Covered Total %
statement 209 223 93.7
branch 36 48 75.0
condition 7 9 77.7
subroutine 22 23 95.6
pod 4 15 26.6
total 278 318 87.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Geo::Google::PolylineEncoder - encode lat/lons to Google Maps Polylines
4              
5             =head1 SYNOPSIS
6              
7             use Geo::Google::PolylineEncoder;
8              
9             my $points = [
10             # can also take points as [lat, lon]
11             { lat => 38.5, lon => -120.2 },
12             { lat => 40.7, lon => -120.95 },
13             { lat => 43.252, lon => -126.453 },
14             ];
15             my $encoder = Geo::Google::PolylineEncoder->new;
16             my $eline = $encoder->encode( $points );
17             print $eline->{num_levels}; # 18
18             print $eline->{zoom_factor}; # 2
19             print $eline->{points}; # _p~iF~ps|U_ulLnnqC_mqNvxq`@
20             print $eline->{levels}; # POP
21              
22             # in Javascript, assuming eline was encoded as JSON:
23             # ... load GMap2 ...
24             var opts = {
25             points: eline.points,
26             levels: eline.levels,
27             numLevels: eline.num_levels,
28             zoomFactor: eline.zoom_factor,
29             };
30             var line = GPolyline.fromEncoded( opts );
31              
32             =cut
33              
34             package Geo::Google::PolylineEncoder;
35              
36 3     3   2804 use strict;
  3         9  
  3         111  
37 3     3   21 use warnings;
  3         5  
  3         147  
38              
39 3         20 use accessors qw(num_levels zoom_factor visible_threshold force_endpoints
40             zoom_level_breaks escape_encoded_points lons_first
41 3     3   2270 points dists max_dist encoded_points encoded_levels );
  3         3425  
42 3         5401 use constant defaults => {
43             num_levels => 18,
44             zoom_factor => 2,
45             force_endpoints => 1,
46             escape_encoded_points => 0,
47             visible_threshold => 0.00001,
48             lons_first => 0,
49 3     3   1066 };
  3         5  
50             our $VERSION = 0.06;
51              
52             # The constructor
53             sub new {
54 10     10 1 191620 my $class = shift;
55 10         48 my $self = bless {}, $class;
56 10         49 $self->init(@_);
57 10         66 return $self;
58             }
59              
60             sub init {
61 10     10 0 37 my ($self, %args) = @_;
62              
63 10         17 foreach my $attr (keys %{ $self->defaults }) {
  10         82  
64 60         441 $self->$attr($self->defaults->{$attr});
65             }
66              
67 10         75 foreach my $attr (keys %args) {
68 21         99 $self->$attr($args{$attr});
69             }
70             }
71              
72             sub init_zoom_level_breaks {
73 10     10 0 30 my $self = shift;
74              
75             # Cache for performance:
76 10         28 my $num_levels = $self->num_levels;
77              
78 10         44 my @zoom_level_breaks;
79 10         25 for my $i (1 .. $num_levels) {
80 171         1162 push @zoom_level_breaks,
81             $self->visible_threshold * $self->zoom_factor ** ($num_levels - $i);
82             }
83              
84 10         106 $self->zoom_level_breaks(\@zoom_level_breaks);
85             }
86              
87             sub reset_encoder {
88 10     10 0 18 my $self = shift;
89 10         43 $self->points([])->dists([])->max_dist(0)->encoded_points('')->encoded_levels('');
90             # Note: calculate zoom level breaks here in case num_levels, etc. have
91             # changed between encodes:
92 10         193 $self->init_zoom_level_breaks;
93             }
94              
95             sub set_points {
96 10     10 0 91 my ($self, $points) = @_;
97              
98 10 50       58 die "points must be an arrayref!" unless UNIVERSAL::isa( $points, 'ARRAY' );
99              
100             # Internally, points are stored as [lat, lon]. Although this is less
101             # readable, it is more efficient than using a hash.
102              
103             # Make a copy of the points we were given
104 10         12 my @points;
105 10 100       510 if (UNIVERSAL::isa($points->[0], 'HASH')) {
    50          
106 8         14 my @keys = keys %{ $points->[0] };
  8         32  
107 8         51 my ($lat_key) = grep( /^lat$/i, @keys );
108 8         47 my ($lon_key) = grep( /^(?:lon)|(?:lng)$/i, @keys );
109 8         28 @points = map { [$_->{$lat_key}, $_->{$lon_key}] } @$points;
  1577         4915  
110             } elsif (UNIVERSAL::isa($points->[0], 'ARRAY')) {
111 2 100       7 if ($self->lons_first) {
112 1         6 @points = map {[ $_->[1], $_->[0] ]} @$points;
  2         7  
113             } else {
114 1         7 @points = map {[ $_->[0], $_->[1] ]} @$points;
  2         7  
115             }
116             } else {
117 0         0 die "don't know how to handle points = $points";
118             }
119              
120 10         87 return $self->points( \@points );
121              
122 0         0 return $self;
123             }
124              
125             # The main entry point
126             sub encode {
127 10     10 1 10446 my ($self, $points) = @_;
128              
129 10         34 $self->reset_encoder
130             ->set_points( $points )
131             ->calculate_distances
132             ->encode_points
133             ->encode_levels;
134              
135 10         87 my $eline = {
136             points => $self->encoded_points,
137             levels => $self->encoded_levels,
138             num_levels => $self->num_levels,
139             zoom_factor => $self->zoom_factor,
140             };
141              
142 10 50       155 if ($self->escape_encoded_points) {
143             # create string literals:
144 0         0 $eline->{points} =~ s/\\/\\\\/g;
145             }
146              
147 10         69 return $eline;
148             }
149              
150             # The main function. Essentially the Douglas-Peucker algorithm, adapted for
151             # encoding. Rather than simply eliminating points, we record their distance
152             # from the segment which occurs at that recursive step. These distances are
153             # then easily converted to zoom levels.
154             #
155             # Note: this function has been optimized and is quite long, sorry.
156             # Any further optimizations should probably be done in XS.
157             sub calculate_distances {
158 10     10 0 108 my $self = shift;
159 10         645 my $points = $self->points;
160              
161 10         41 my @dists;
162 10         15 my $max_dist = 0;
163              
164 10 100       32 if (@$points <= 2) {
165             # no point doing distance calcs:
166 3         9 return $self->dists( \@dists )->max_dist( $max_dist );
167             }
168              
169             # cache commonly used vars:
170 7         19 my $visible_threshold = $self->visible_threshold;
171              
172             # Iterate through all the points, and calculate their dists
173              
174             # Each stack element contains the index of two points representing a line
175             # seg that we calculate distances from. Start off with the first & last pt:
176 7         43 my @stack = ([0, @$points - 1]);
177              
178 7         30 while (@stack > 0) {
179 2375         3005 my $current = pop @stack;
180              
181             # cache to save array lookups:
182 2375         3098 my $current_0 = $current->[0];
183 2375         2501 my $current_1 = $current->[1];
184              
185             # Get the two points, $A & $B:
186 2375         3302 my ($A, $B) = ($points->[$current_0], $points->[$current_1]);
187              
188             # Cache their lon/lats to avoid unneccessary array lookups.
189             # Note: we use X/Y because it's shorter, and more math-y
190 2375         3935 my ($Ax, $Ay, $Bx, $By) = ($A->[1], $A->[0], $B->[1], $B->[0]);
191              
192             # Create a line segment between $A & $B and calculate its length
193             # Note: cache the square of the seg length for use in calcs later...
194 2375         6858 my $seg_length_squared = (($Bx - $Ax) ** 2 + ($By - $Ay) ** 2);
195 2375         2862 my $seg_length = sqrt($seg_length_squared);
196 2375         3228 my $seg_length_is_0 = $seg_length == 0; # cache
197              
198             # Cache the deltas in x/y for calcs later:
199 2375         3298 my ($Bx_minus_Ax, $By_minus_Ay) = ($Bx - $Ax, $By - $Ay);
200              
201 2375         2366 my $current_max_dist = 0;
202 2375         2236 my $current_max_dist_idx;
203 2375         5423 for (my $i = $current_0 + 1; $i < $current_1; $i++) {
204             # Get the current point:
205 16721         22251 my $P = $points->[$i];
206              
207             # Cache its lon/lat to avoid unneccessary hash lookups.
208             # Note: we use X/Y because it's shorter, and more math-y
209 16721         32782 my ($Py, $Px) = ($P->[0], $P->[1]);
210              
211             # Compute the distance between point $P and line segment [$A, $B].
212             # Maths borrowed from Philip Nicoletti (see below).
213             #
214             # Note: we approximate distance using flat (Euclidian) geometry,
215             # rather than trying to bring the curvature of the earth into it.
216             # This greatly simplifies things, and makes the calcs faster...
217             #
218             # Note: distance calculations have been brought in-line as the
219             # majority of encoding time was spent calling the 'distance'
220             # method. This way we can avoid passing lots of data by value,
221             # setting up the sub stack, and we can also cache some values.
222             #my $dist = $self->distance($points->[$i], $A, $B, $seg_length, $seg_length_squared);
223              
224 16721         16406 my $dist;
225 16721 50       22919 if ($seg_length_is_0) {
226             # The line is really just a point, so calc dist between it and $P:
227 0         0 $dist = sqrt(($By - $Py) ** 2 + ($Bx - $Px) ** 2);
228             } else {
229             # Thanks to Philip Nicoletti's explanation:
230             # http://www.codeguru.com/forum/printthread.php?t=194400
231             #
232             # So, to find out how far the line segment (AB) is from the point (P),
233             # let 'I' be the point of perpendicular projection of P on AB. The
234             # parameter 'r' indicates I's position along AB, and is computed by
235             # the dot product of AP and AB divided by the square of the length
236             # of AB:
237             #
238             # AP . AB (Px-Ax)(Bx-Ax) + (Py-Ay)(By-Ay)
239             # r = -------- = -------------------------------
240             # ||AB||^2 L^2
241             #
242             # r can be interpreded ala:
243             #
244             # r=0 I = A
245             # r=1 I = B
246             # r<0 I is on the backward extension of A-B
247             # r>1 I is on the forward extension of A-B
248             # 0
249             #
250             # In cases 1-4 we can simply use the distance between P and either A or B.
251             # In case 5 we can use the distance between I and P. To do that we need to
252             # find I:
253             #
254             # Ix = Ax + r(Bx-Ax)
255             # Iy = Ay + r(By-Ay)
256             #
257             # And the distance from A to I = r*L.
258             # Use another parameter s to indicate the location along IP, with the
259             # following meaning:
260             # s<0 P is left of AB
261             # s>0 P is right of AB
262             # s=0 P is on AB
263             #
264             # Compute s as follows:
265             #
266             # (Ay-Py)(Bx-Ax) - (Ax-Px)(By-Ay)
267             # s = -------------------------------
268             # L^2
269             #
270             # Then the distance from P to I = |s|*L.
271              
272 16721         40764 my $r = (($Px - $Ax) * ($Bx - $Ax) +
273             ($Py - $Ay) * ($By - $Ay)) / $seg_length_squared;
274              
275 16721 100       34731 if ($r <= 0.0) {
    100          
276             # Either I = A, or I is on the backward extension of A-B,
277             # so find dist between $P & $A:
278 4         16 $dist = sqrt(($Ay - $Py) ** 2 + ($Ax - $Px) ** 2);
279             } elsif ($r >= 1.0) {
280             # Either I = B, or I is on the forward extension of A-B,
281             # so find dist between $P & $B:
282 116         220 $dist = sqrt(($By - $Py) ** 2 + ($Bx - $Px) ** 2);
283             } else {
284             # I is interior to A-B, so find $s, and use it to find the
285             # dist between $P and A-B:
286 16601         26115 my $s = (($Ay - $Py) * $Bx_minus_Ax -
287             ($Ax - $Px) * $By_minus_Ay) / $seg_length_squared;
288 16601         24471 $dist = abs($s) * $seg_length;
289             }
290             # warn "\t$Px\t$Py\t$Ax\t$Ay\t$Bx\t$By\t$r\t$dist\n";
291             }
292              
293             # See if this distance is the greatest for this segment so far:
294 16721 100       43004 if ($dist > $current_max_dist) {
295 5388         5659 $current_max_dist = $dist;
296 5388         5291 $current_max_dist_idx = $i;
297 5388 100       16019 if ($current_max_dist > $max_dist) {
298 289         712 $max_dist = $current_max_dist;
299             }
300             }
301             }
302              
303             # If the point that had the greatest distance from the line seg is
304             # also greater than our threshold, process again using it as a new
305             # start/end point for the line.
306 2375 100       6274 if ($current_max_dist > $visible_threshold) {
307             # store this distance - we'll use it later when creating zoom values
308 1184         1838 $dists[$current_max_dist_idx] = $current_max_dist;
309 1184         2248 push @stack, [$current_0, $current_max_dist_idx];
310 1184         4795 push @stack, [$current_max_dist_idx, $current_1];
311             }
312             }
313              
314 7         52 $self->dists( \@dists )->max_dist( $max_dist );
315             } # calculate_distances
316              
317              
318             # The encode_points function is very similar to Google's
319             # http://www.google.com/apis/maps/documentation/polyline.js
320             # The key difference is that not all points are encoded,
321             # since some were eliminated by Douglas-Peucker.
322             sub encode_points {
323 10     10 0 113 my $self = shift;
324 10         32 my $points = $self->points;
325 10         60 my $dists = $self->dists;
326              
327 10         36 my $encoded_points = "";
328 10         17 my $oldencoded_points = "";
329 10         17 my ($last_lat, $last_lon) = (0.0, 0.0);
330              
331 10         54 for (my $i = 0; $i < @$points; $i++) {
332 1581         1929 my $point = $points->[$i];
333 1581         1886 my $lat = $point->[0];
334 1581         1674 my $lon = $point->[1];
335              
336 1581 100 100     5561 if (defined($dists->[$i]) || $i == 0 || $i == @$points - 1) {
      100        
337             # compute deltas, rounded to 5 decimal places:
338 1204         5193 my $lat_e5 = sprintf('%.5f', $lat)+0; # round()
339 1204         4321 my $lon_e5 = sprintf('%.5f', $lon)+0; # round()
340 1204         3870 my $delta_lat = sprintf('%.5f', $lat_e5 - $last_lat)+0;
341 1204         3652 my $delta_lon = sprintf('%.5f', $lon_e5 - $last_lon)+0;
342 1204         1403 ($last_lat, $last_lon) = ($lat_e5, $lon_e5);
343              
344 1204         2246 $encoded_points .=
345             $self->encode_signed_number($delta_lat) .
346             $self->encode_signed_number($delta_lon);
347             } else {
348             # warn "skipping point: $lat, $lon";
349             }
350             }
351              
352 10         43 $self->encoded_points( $encoded_points );
353             }
354              
355              
356             # Use compute_level to march down the list of points and encode the levels.
357             # Like encode_points, we ignore points whose distance (in dists) is undefined.
358             # See http://code.google.com/apis/maps/documentation/polylinealgorithm.html
359             sub encode_levels {
360 10     10 0 91 my $self = shift;
361 10         30 my $points = $self->points;
362 10         49 my $dists = $self->dists;
363 10         53 my $max_dist = $self->max_dist;
364              
365             # Cache for performance:
366 10         52 my $num_levels = $self->num_levels;
367 10         42 my $num_levels_minus_1 = $num_levels - 1;
368 10         25 my $visible_threshold = $self->visible_threshold;
369 10         54 my $zoom_level_breaks = $self->zoom_level_breaks;
370              
371 10         38 my $encoded_levels = "";
372              
373 10 50       27 if ($self->force_endpoints) {
374 10         91 $encoded_levels .= $self->encode_number($num_levels_minus_1);
375             } else {
376 0         0 $encoded_levels .= $self->encode_number($num_levels_minus_1 - $self->compute_level($max_dist));
377             }
378              
379              
380             # Note: skip the first & last point:
381 10         32 for my $i (1 .. scalar(@$points) - 2) {
382 1561         1889 my $dist = $dists->[$i];
383 1561 100       2445 if (defined $dist) {
384             # Note: brought compute_level in-line as it was performing *really* slowly
385             #
386             # This computes the appropriate zoom level of a point in terms of it's
387             # distance from the relevant segment in the DP algorithm. Could be done
388             # in terms of a logarithm, but this approach makes it a bit easier to
389             # ensure that the level is not too large.
390             #my $level = $self->compute_level($dist);
391 1184         1511 my $level = 0;
392 1184 50       1916 if ($dist > $visible_threshold) {
393 1184         1957 while ($dist < $zoom_level_breaks->[$level]) {
394 12431         18702 $level++;
395             }
396             }
397              
398 1184         2189 $encoded_levels .= $self->encode_number($num_levels_minus_1 - $level);
399             }
400             }
401              
402 10 50       41 if ($self->force_endpoints) {
403 10         66 $encoded_levels .= $self->encode_number($num_levels_minus_1);
404             } else {
405 0         0 $encoded_levels .= $self->encode_number($num_levels_minus_1 - $self->compute_level($max_dist));
406             }
407              
408 10         34 $self->encoded_levels( $encoded_levels );
409             }
410              
411              
412             # This computes the appropriate zoom level of a point in terms of it's
413             # distance from the relevant segment in the DP algorithm. Could be done
414             # in terms of a logarithm, but this approach makes it a bit easier to
415             # ensure that the level is not too large.
416             sub compute_level {
417 0     0 0 0 my ($self, $dist) = @_;
418              
419             # Cache for performance:
420 0         0 my $zoom_level_breaks = $self->zoom_level_breaks;
421              
422 0         0 my $level;
423 0 0       0 if ($dist > $self->visible_threshold) {
424 0         0 $level = 0;
425 0         0 while ($dist < $zoom_level_breaks->[$level]) {
426 0         0 $level++;
427             }
428             }
429              
430 0         0 return $level;
431             }
432              
433             # Based on the official google example
434             # http://code.google.com/apis/maps/documentation/include/polyline.js
435             sub encode_signed_number {
436 2412     2412 0 3055 my ($self, $orig_num) = @_;
437              
438             # 1. Take the initial signed value:
439             # 2. Take the decimal value and multiply it by 1e5, rounding the result:
440              
441             # Note 1: we limit the number to 5 decimal places with sprintf to avoid
442             # perl's rounding errors (they can throw the line off by a big margin sometimes)
443             # From Geo::Google: use the correct floating point precision or else
444             # 34.06694 - 34.06698 will give you -3.999999999999999057E-5 which doesn't
445             # encode properly. -4E-5 encodes properly.
446              
447             # Note 2: we use sprintf(%.0f ...) rather than int() for similar reasons
448             # (see perldoc -f int), though there's not much in it and the sprintf approach
449             # ends up doing more of a round() than a floor() in some cases:
450             # floor = -30 num=-30 *int=-29 1e5=-30 %3.5f=-0.00030 orig=-0.000300000000009959
451             # floor = 119 *num=120 int=119 1e5=120 %3.5f=0.00120 orig=0.0011999999999972
452              
453             # Note 3: We don't use floor() to avoid a dependency on POSIX. And it
454             # doesn't round() anyway.
455              
456             # do this in a series of steps so we can see what's going on in the debugger:
457 2412         7911 my $num3_5 = sprintf('%.5f', $orig_num)+0; # round at 5 decimal places
458 2412         2836 my $num_1e5 = $num3_5 * 1e5;
459 2412         3636 my $num = sprintf('%.0f', $num_1e5)+0; # think int(...)
460              
461             # RT 49327: the signedness has to be determined *after* rounding
462 2412         2663 my $is_negative = $num < 0;
463              
464             {
465             # 3. Convert the decimal value to binary. Note that a negative value
466             # must be calculated using its two's complement by inverting the
467             # binary value and adding one to the result.
468              
469             # Note: perl ints are already binary, but bitwise operators work on
470             # the assumption they are unsigned, ie ~$num => one's complement.
471             # if we 'use integer' bitwise operands are treated as signed:
472 3     3   2553 use integer; # force 2's complement
  3         28  
  3         16  
  2412         2075  
473              
474             # 4. Left-shift the binary value one bit:
475 2412         2141 $num = $num << 1;
476              
477             # 5. If the original decimal value is negative, invert this encoding:
478             # (see note on RT 49327 above)
479 2412 100       4282 if ($is_negative) {
480 626         765 $num = ~$num;
481             }
482             }
483              
484 2412         4055 return $self->encode_number($num);
485             }
486              
487             # Based on the official google example
488             # http://code.google.com/apis/maps/documentation/include/polyline.js
489             sub encode_number {
490 3618     3618 0 3940 my ($self, $num) = @_;
491 3     3   234 no integer; # treat bitwise operands as unsigned
  3         6  
  3         11  
492              
493             # 6. Break the binary value out into 5-bit chunks (starting from the right hand side):
494             # 7. Place the 5-bit chunks into reverse order:
495             # 8. OR each value with 0x20 if another bit chunk follows:
496             # 9. Convert each value to decimal:
497             # 10. Add 63 to each value:
498              
499 3618         3684 my $encodeString = "";
500 3618         6371 while ($num >= 0x20) {
501 1974         2296 my $nextValue = (0x20 | ($num & 0x1f)) + 63;
502 1974         2246 $encodeString .= chr( $nextValue );
503 1974         3580 $num >>= 5;
504             }
505              
506 3618         3572 my $finalValue = $num + 63;
507 3618         3760 $encodeString .= chr( $finalValue );
508              
509 3618         11125 return $encodeString;
510             }
511              
512             # Superficial validation of encoded points. Note that decode_points
513             # does not check that points are validated before decoding.
514             sub validate_encoded_points {
515 6     6 0 6053 my ($class, $encoded) = @_;
516              
517 6 50 33     36 return unless (defined $encoded && $encoded ne "");
518              
519 6         51 my @ords = unpack "c*", $encoded;
520              
521 6 50       15 my @out = grep { $_ < 63 || $_ > 127 } @ords;
  180         626  
522 6 50       16 return if @out;
523              
524 6         35 return 1;
525             }
526              
527             # Decode an encoded polyline into a list of lat/lng tuples.
528             # adapted from http://code.google.com/apis/maps/documentation/include/polyline.js
529             sub decode_points {
530 12     12 1 16921 my ($class, $encoded) = @_;
531              
532 12         23 my $len = length( $encoded );
533 12         21 my @array;
534              
535 12         14 my $index = 0;
536 12         17 my $lat = 0;
537 12         16 my $lon = 0;
538              
539 12         35 while ($index < $len) {
540             {
541 2339         2330 my $b;
  2339         2184  
542 2339         2442 my $shift = 0;
543 2339         2450 my $result = 0;
544 2339         2124 do {
545 4194         4909 $b = ord( substr( $encoded, $index++, 1 ) ) - 63;
546 4194         4456 $result |= ($b & 0x1f) << $shift;
547 4194         7741 $shift += 5;
548             } while ($b >= 0x20);
549 2339         2470 my $dlat = $result >> 1;
550 2339 100       3833 if ($result & 1) {
551 3     3   847 use integer; # force 2's complement
  3         12  
  3         11  
552 706         788 $dlat = ~$dlat;
553             }
554 2339         2281 $lat += $dlat;
555              
556             # cut-n-paste to improve performance?
557 2339         2229 $shift = 0;
558 2339         2276 $result = 0;
559 2339         2081 do {
560 4248         4687 $b = ord( substr( $encoded, $index++, 1 ) ) - 63;
561 4248         4485 $result |= ($b & 0x1f) << $shift;
562 4248         7621 $shift += 5;
563             } while ($b >= 0x20);
564 2339         2400 my $dlon = $result >> 1;
565 2339 100       4370 if ($result & 1) {
566 3     3   248 use integer; # force 2's complement
  3         7  
  3         17  
567 498         591 $dlon = ~$dlon;
568             }
569 2339         2838 $lon += $dlon;
570             }
571              
572 2339         9468 push @array, { lat => $lat * 1e-5, lon => $lon * 1e-5 };
573             }
574              
575 12         266 return \@array;
576             }
577              
578             # Decode an encoded levels string into a list of levels.
579             # adapted from http://code.google.com/apis/maps/documentation/include/polyline.js
580             sub decode_levels {
581 10     10 1 83 my ($class, $encoded) = @_;
582              
583 10         18 my $len = length( $encoded );
584 10         19 my @levels;
585              
586 10         31 for (my $index = 0; $index < $len; $index++) {
587 2317         2459 my $level = ord( substr( $encoded, $index, 1 ) ) - 63;
588 2317         4478 push @levels, $level;
589             }
590              
591 10         36 return \@levels;
592             }
593              
594              
595             1;
596              
597             __END__