File Coverage

blib/lib/Algorithm/GooglePolylineEncoding.pm
Criterion Covered Total %
statement 74 74 100.0
branch 10 10 100.0
condition n/a
subroutine 8 8 100.0
pod 4 4 100.0
total 96 96 100.0


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Author: Slaven Rezic
5             #
6             # Copyright (C) 2009,2010,2012,2017 Slaven Rezic. All rights reserved.
7             # This package is free software; you can redistribute it and/or
8             # modify it under the same terms as Perl itself.
9             #
10             # Mail: slaven@rezic.de
11             # WWW: http://www.rezic.de/eserte/
12             #
13              
14             package Algorithm::GooglePolylineEncoding;
15              
16 1     1   804 use 5.006; # sprintf("%b")
  1         4  
17              
18 1     1   5 use strict;
  1         2  
  1         20  
19 1     1   4 use vars qw($VERSION);
  1         2  
  1         524  
20             $VERSION = '0.05';
21              
22             sub encode_number {
23             # 1. Take the initial signed value:
24             # -179.9832104
25 48     48 1 81 my $number = shift;
26             # 2. Take the decimal value and multiply it by 1e5, rounding the result:
27             # -17998321
28 48 100       113 $number = int($number * 1e5 + ($number < 0 ? -0.5 : 0.5));
29             # Don't do this before rounding. Negativeness may change if for example
30             # using very small negative numbers.
31 48         81 my $is_negative = $number < 0;
32             # 3. Convert the decimal value to binary. Note that a negative value must be calculated using its two's complement by inverting the binary value and adding one to the result:
33             # 00000001 00010010 10100001 11110001
34             # 11111110 11101101 01011110 00001110
35             # 11111110 11101101 01011110 00001111
36             # nothing to do here, we don't calculate with binary strings...
37             # 4. Left-shift the binary value one bit:
38             # 11111101 11011010 10111100 00011110
39 48         71 $number <<= 1;
40 48         84 $number &= 0xffffffff; # to assure 32 bit
41             # 5. If the original decimal value is negative, invert this encoding:
42             # 00000010 00100101 01000011 11100001
43 48 100       104 if ($is_negative) {
44 24         35 $number = (~$number);
45 24         39 $number &= 0xffffffff;
46             }
47             # 6. Break the binary value out into 5-bit chunks (starting from the right hand side):
48             # 00001 00010 01010 10000 11111 00001
49 48         109 my $bin = sprintf '%b', $number;
50 48 100       139 $bin = '0'x(5-length($bin)%5) . $bin if length($bin)%5 != 0; # pad
51 48         73 my @chunks;
52 48         90 my $revbin = reverse $bin;
53 48         432 push @chunks, scalar reverse($1) while $revbin =~ m{(.....)}g;
54             # 7. Place the 5-bit chunks into reverse order:
55             # 00001 11111 10000 01010 00010 00001
56             # It's already reversed
57             # 8. OR each value with 0x20 if another bit chunk follows:
58             # 100001 111111 110000 101010 100010 000001
59 48         113 @chunks = ((map { oct("0b$_") | 0x20 } @chunks[0 .. $#chunks-1]), oct("0b".$chunks[-1])); # and also decode to decimal on the fly
  179         360  
60             # 9. Convert each value to decimal:
61             # 33 63 48 42 34 1
62             # Done above
63             # 10. Add 63 to each value:
64             # 96 126 111 105 97 64
65 48         89 @chunks = map { $_+63 } @chunks;
  227         394  
66             # 11. Convert each value to its ASCII equivalent:
67             # `~oia@
68 48         82 @chunks = map { chr } @chunks;
  227         447  
69 48         162 join '', @chunks;
70             }
71              
72             sub encode_polyline {
73 2     2 1 412 my(@path) = @_;
74 2         4 my @res;
75 2         4 my($curr_lat_e5,$curr_lon_e5) = (0,0);
76 2         5 for my $lat_lon (@path) {
77 23         46 my($lat_e5,$lon_e5) = map { sprintf("%.0f", $_*1e5) } ($lat_lon->{lat}, $lat_lon->{lon});
  46         115  
78 23         55 my $deltay = ($lat_e5 - $curr_lat_e5) / 1e5;
79 23         38 my $deltax = ($lon_e5 - $curr_lon_e5) / 1e5;
80 23         44 push @res, encode_number($deltay), encode_number($deltax);
81 23         51 ($curr_lat_e5,$curr_lon_e5) = ($lat_e5,$lon_e5);
82             }
83 2         13 join '', @res;
84             }
85              
86             sub encode_level {
87             # 1. Take the initial unsigned value:
88             # 174
89 2     2 1 5 my $number = shift;
90             # 2. Convert the decimal value to a binary value:
91             # 10101110
92 2         9 my $bin = sprintf '%b', $number;
93             # 3. Break the binary value out into 5-bit chunks (starting from the right hand side):
94             # 101 01110
95 2 100       8 $bin = '0'x(5-length($bin)%5) . $bin if length($bin)%5 != 0; # pad
96 2         4 my @chunks;
97 2         5 my $revbin = reverse $bin;
98 2         44 push @chunks, scalar reverse($1) while $revbin =~ m{(.....)}g;
99             # 4. Place the 5-bit chunks into reverse order:
100             # 01110 101
101             # It's already reversed
102             # 5. OR each value with 0x20 if another bit chunk follows:
103             # 101110 00101
104 2         7 @chunks = ((map { oct("0b$_") | 0x20 } @chunks[0 .. $#chunks-1]), oct("0b".$chunks[-1])); # and also decode to decimal on the fly
  8         24  
105             # 6. Convert each value to decimal:
106             # 46 5
107             # Done above
108             # 7. Add 63 to each value:
109             # 109 68
110 2         5 @chunks = map { $_+63 } @chunks;
  10         18  
111             # 8. Convert each value to its ASCII equivalent:
112             # mD
113 2         4 @chunks = map { chr } @chunks;
  10         22  
114 2         13 join '', @chunks;
115             }
116              
117             # Translated this php script
118             #
119             # to perl
120             sub decode_polyline {
121 2     2 1 8 my($encoded) = @_;
122              
123 2         4 my $length = length $encoded;
124 2         3 my $index = 0;
125 2         4 my @points;
126 2         3 my $lat = 0;
127 2         4 my $lng = 0;
128              
129 2         6 while ($index < $length) {
130             # The encoded polyline consists of a latitude value followed
131             # by a longitude value. They should always come in pairs. Read
132             # the latitude value first.
133 23         44 for my $val (\$lat, \$lng) {
134 46         70 my $shift = 0;
135 46         61 my $result = 0;
136             # Temporary variable to hold each ASCII byte.
137 46         69 my $b;
138 46         64 do {
139             # The `ord(substr($encoded, $index++))` statement returns
140             # the ASCII code for the character at $index. Subtract 63
141             # to get the original value. (63 was added to ensure
142             # proper ASCII characters are displayed in the encoded
143             # polyline string, which is `human` readable)
144 225         359 $b = ord(substr($encoded, $index++, 1)) - 63;
145              
146             # AND the bits of the byte with 0x1f to get the original
147             # 5-bit `chunk. Then left shift the bits by the required
148             # amount, which increases by 5 bits each time. OR the
149             # value into $results, which sums up the individual 5-bit
150             # chunks into the original value. Since the 5-bit chunks
151             # were reversed in order during encoding, reading them in
152             # this way ensures proper summation.
153 225         324 $result |= ($b & 0x1f) << $shift;
154 225         462 $shift += 5;
155             }
156             # Continue while the read byte is >= 0x20 since the last
157             # `chunk` was not OR'd with 0x20 during the conversion
158             # process. (Signals the end)
159             while ($b >= 0x20);
160              
161 1     1   425 use integer; # see last paragraph of "Integer Arithmetic" in perlop.pod
  1         16  
  1         5  
162              
163             # Check if negative, and convert. (All negative values have the last bit
164             # set)
165 46 100       89 my $dtmp = (($result & 1) ? ~($result >> 1) : ($result >> 1));
166              
167             # Compute actual latitude (resp. longitude) since value is
168             # offset from previous value.
169 46         85 $$val += $dtmp;
170             }
171              
172             # The actual latitude and longitude values were multiplied by
173             # 1e5 before encoding so that they could be converted to a 32-bit
174             # integer representation. (With a decimal accuracy of 5 places)
175             # Convert back to original values.
176 23         76 push @points, {lat => $lat * 1e-5, lon => $lng * 1e-5};
177             }
178              
179 2         10 @points;
180             }
181              
182             1;
183              
184             __END__